Feature/respect data source in symbol data endpoint (#370)

* Respect data source in symbol data endpoint

* Respect data source in the data provider service

* Combine symbol with data source in get() of data provider service

* Improve search functionality for multiple data sources

* Update changelog
This commit is contained in:
Thomas Kaul 2021-09-18 19:32:22 +02:00 committed by GitHub
parent 641fe4e8f4
commit 0f72673ef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 387 additions and 181 deletions

View File

@ -7,11 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added the data source attribute to the symbol profile model
### Changed
- Respected the data source attribute in the data provider service
- Respected the data source attribute in the symbol data endpoint
- Improved the search functionality of the data management (multiple data sources)
### Fixed ### Fixed
- Hid the net performance in the _Presenter View_ (portfolio holdings and summary tab on the home page) - Hid the net performance in the _Presenter View_ (portfolio holdings and summary tab on the home page)
- Hid the sign if the performance is zero in the value component - Hid the sign if the performance is zero in the value component
### Todo
- Apply data migration (`yarn database:push`)
## 1.53.0 - 13.09.2021 ## 1.53.0 - 13.09.2021
### Changed ### Changed

View File

@ -56,7 +56,9 @@ export class OrderService {
]); ]);
} }
this.dataGatheringService.gatherProfileData([data.symbol]); this.dataGatheringService.gatherProfileData([
{ dataSource: data.dataSource, symbol: data.symbol }
]);
await this.cacheService.flush(); await this.cacheService.flush();

View File

@ -1,6 +1,6 @@
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 { Currency, MarketData } from '@prisma/client'; import { Currency, DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { MarketDataService } from './market-data.service'; import { MarketDataService } from './market-data.service';
@ -14,6 +14,7 @@ jest.mock('./market-data.service', () => {
date, date,
symbol, symbol,
createdAt: date, createdAt: date,
dataSource: DataSource.YAHOO,
id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584', id: 'aefcbe3a-ee10-4c4f-9f2d-8ffad7b05584',
marketPrice: 1847.839966 marketPrice: 1847.839966
}); });
@ -30,6 +31,7 @@ jest.mock('./market-data.service', () => {
return Promise.resolve<MarketData[]>([ return Promise.resolve<MarketData[]>([
{ {
createdAt: dateRangeStart, createdAt: dateRangeStart,
dataSource: DataSource.YAHOO,
date: dateRangeStart, date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d', id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902, marketPrice: 1841.823902,
@ -37,6 +39,7 @@ jest.mock('./market-data.service', () => {
}, },
{ {
createdAt: dateRangeEnd, createdAt: dateRangeEnd,
dataSource: DataSource.YAHOO,
date: dateRangeEnd, date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56', id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966, marketPrice: 1847.839966,
@ -106,11 +109,11 @@ describe('CurrentRateService', () => {
expect( expect(
await currentRateService.getValues({ await currentRateService.getValues({
currencies: { AMZN: Currency.USD }, currencies: { AMZN: Currency.USD },
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: { dateQuery: {
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)), lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)) gte: new Date(Date.UTC(2020, 0, 1, 0, 0, 0))
}, },
symbols: ['AMZN'],
userCurrency: Currency.CHF userCurrency: Currency.CHF
}) })
).toMatchObject([ ).toMatchObject([

View File

@ -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 { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
@ -25,7 +26,9 @@ export class CurrentRateService {
userCurrency userCurrency
}: GetValueParams): Promise<GetValueObject> { }: GetValueParams): Promise<GetValueObject> {
if (isToday(date)) { if (isToday(date)) {
const dataProviderResult = await this.dataProviderService.get([symbol]); const dataProviderResult = await this.dataProviderService.get([
{ symbol, dataSource: DataSource.YAHOO }
]);
return { return {
date: resetHours(date), date: resetHours(date),
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0, marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0,
@ -55,8 +58,8 @@ export class CurrentRateService {
public async getValues({ public async getValues({
currencies, currencies,
dataGatheringItems,
dateQuery, dateQuery,
symbols,
userCurrency userCurrency
}: GetValuesParams): Promise<GetValueObject[]> { }: GetValuesParams): Promise<GetValueObject[]> {
const includeToday = const includeToday =
@ -75,24 +78,31 @@ export class CurrentRateService {
if (includeToday) { if (includeToday) {
const today = resetHours(new Date()); const today = resetHours(new Date());
promises.push( promises.push(
this.dataProviderService.get(symbols).then((dataResultProvider) => { this.dataProviderService
const result = []; .get(dataGatheringItems)
for (const symbol of symbols) { .then((dataResultProvider) => {
result.push({ const result = [];
symbol, for (const dataGatheringItem of dataGatheringItems) {
date: today, result.push({
marketPrice: this.exchangeRateDataService.toCurrency( date: today,
dataResultProvider?.[symbol]?.marketPrice ?? 0, marketPrice: this.exchangeRateDataService.toCurrency(
dataResultProvider?.[symbol]?.currency, dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice ??
userCurrency 0,
) dataResultProvider?.[dataGatheringItem.symbol]?.currency,
}); userCurrency
} ),
return result; symbol: dataGatheringItem.symbol
}) });
}
return result;
})
); );
} }
const symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
promises.push( promises.push(
this.marketDataService this.marketDataService
.getRange({ .getRange({

View File

@ -1,10 +1,11 @@
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { DateQuery } from './date-query.interface'; import { DateQuery } from './date-query.interface';
export interface GetValuesParams { export interface GetValuesParams {
currencies: { [symbol: string]: Currency }; currencies: { [symbol: string]: Currency };
dataGatheringItems: IDataGatheringItem[];
dateQuery: DateQuery; dateQuery: DateQuery;
symbols: string[];
userCurrency: Currency; userCurrency: Currency;
} }

View File

@ -1,10 +1,11 @@
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
export interface PortfolioOrder { export interface PortfolioOrder {
currency: Currency; currency: Currency;
date: string; date: string;
dataSource: DataSource;
fee: Big; fee: Big;
name: string; name: string;
quantity: Big; quantity: Big;

View File

@ -1,8 +1,9 @@
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
export interface TransactionPointSymbol { export interface TransactionPointSymbol {
currency: Currency; currency: Currency;
dataSource: DataSource;
fee: Big; fee: Big;
firstBuyDate: string; firstBuyDate: string;
investment: Big; investment: Big;

View File

@ -1,7 +1,7 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
addDays, addDays,
@ -85,7 +85,7 @@ jest.mock('./current-rate.service', () => {
getValues: ({ getValues: ({
currencies, currencies,
dateQuery, dateQuery,
symbols, dataGatheringItems,
userCurrency userCurrency
}: GetValuesParams) => { }: GetValuesParams) => {
const result = []; const result = [];
@ -95,21 +95,23 @@ jest.mock('./current-rate.service', () => {
isBefore(date, endOfDay(dateQuery.lt)); isBefore(date, endOfDay(dateQuery.lt));
date = addDays(date, 1) date = addDays(date, 1)
) { ) {
for (const symbol of symbols) { for (const dataGatheringItem of dataGatheringItems) {
result.push({ result.push({
date, date,
symbol, marketPrice: mockGetValue(dataGatheringItem.symbol, date)
marketPrice: mockGetValue(symbol, date).marketPrice .marketPrice,
symbol: dataGatheringItem.symbol
}); });
} }
} }
} else { } else {
for (const date of dateQuery.in) { for (const date of dateQuery.in) {
for (const symbol of symbols) { for (const dataGatheringItem of dataGatheringItems) {
result.push({ result.push({
date, date,
symbol, marketPrice: mockGetValue(dataGatheringItem.symbol, date)
marketPrice: mockGetValue(symbol, date).marketPrice .marketPrice,
symbol: dataGatheringItem.symbol
}); });
} }
} }
@ -148,7 +150,7 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const orders = [ const orders: PortfolioOrder[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
name: 'Vanguard Total Stock Market Index Fund ETF Shares', name: 'Vanguard Total Stock Market Index Fund ETF Shares',
@ -157,6 +159,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('144.38'), unitPrice: new Big('144.38'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('5') fee: new Big('5')
}, },
{ {
@ -167,6 +170,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('147.99'), unitPrice: new Big('147.99'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('10') fee: new Big('10')
}, },
{ {
@ -177,6 +181,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Sell, type: OrderType.Sell,
unitPrice: new Big('151.41'), unitPrice: new Big('151.41'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('5') fee: new Big('5')
} }
]; ];
@ -189,6 +194,7 @@ describe('PortfolioCalculator', () => {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
@ -203,6 +209,7 @@ describe('PortfolioCalculator', () => {
date: '2019-08-03', date: '2019-08-03',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
@ -217,6 +224,7 @@ describe('PortfolioCalculator', () => {
date: '2020-02-02', date: '2020-02-02',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
@ -235,7 +243,7 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const orders = [ const orders: PortfolioOrder[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
name: 'Vanguard Total Stock Market Index Fund ETF Shares', name: 'Vanguard Total Stock Market Index Fund ETF Shares',
@ -244,6 +252,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('144.38'), unitPrice: new Big('144.38'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('5') fee: new Big('5')
}, },
{ {
@ -254,6 +263,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('147.99'), unitPrice: new Big('147.99'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('10') fee: new Big('10')
}, },
{ {
@ -264,6 +274,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Sell, type: OrderType.Sell,
unitPrice: new Big('151.41'), unitPrice: new Big('151.41'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big('5') fee: new Big('5')
} }
]; ];
@ -276,6 +287,7 @@ describe('PortfolioCalculator', () => {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
@ -290,6 +302,7 @@ describe('PortfolioCalculator', () => {
date: '2019-08-03', date: '2019-08-03',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
@ -299,6 +312,7 @@ describe('PortfolioCalculator', () => {
fee: new Big('5') fee: new Big('5')
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTX', symbol: 'VTX',
investment: new Big('1479.9'), investment: new Big('1479.9'),
@ -313,6 +327,7 @@ describe('PortfolioCalculator', () => {
date: '2020-02-02', date: '2020-02-02',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('686.75'), investment: new Big('686.75'),
@ -322,6 +337,7 @@ describe('PortfolioCalculator', () => {
fee: new Big('10') fee: new Big('10')
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTX', symbol: 'VTX',
investment: new Big('1479.9'), investment: new Big('1479.9'),
@ -336,10 +352,11 @@ describe('PortfolioCalculator', () => {
}); });
it('with two orders at the same day of the same type', () => { it('with two orders at the same day of the same type', () => {
const orders = [ const orders: PortfolioOrder[] = [
...ordersVTI, ...ordersVTI,
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
date: '2021-02-01', date: '2021-02-01',
name: 'Vanguard Total Stock Market Index Fund ETF Shares', name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('20'), quantity: new Big('20'),
@ -363,6 +380,7 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
investment: new Big('1443.8'), investment: new Big('1443.8'),
quantity: new Big('10'), quantity: new Big('10'),
@ -377,6 +395,7 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
investment: new Big('2923.7'), investment: new Big('2923.7'),
quantity: new Big('20'), quantity: new Big('20'),
@ -391,6 +410,7 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
investment: new Big('652.55'), investment: new Big('652.55'),
quantity: new Big('5'), quantity: new Big('5'),
@ -405,6 +425,7 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
investment: new Big('6627.05'), investment: new Big('6627.05'),
quantity: new Big('35'), quantity: new Big('35'),
@ -419,6 +440,7 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
investment: new Big('8403.95'), investment: new Big('8403.95'),
quantity: new Big('45'), quantity: new Big('45'),
@ -432,10 +454,11 @@ describe('PortfolioCalculator', () => {
}); });
it('with additional order', () => { it('with additional order', () => {
const orders = [ const orders: PortfolioOrder[] = [
...ordersVTI, ...ordersVTI,
{ {
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
date: '2019-09-01', date: '2019-09-01',
name: 'Amazon.com, Inc.', name: 'Amazon.com, Inc.',
quantity: new Big('5'), quantity: new Big('5'),
@ -458,6 +481,7 @@ describe('PortfolioCalculator', () => {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
@ -472,6 +496,7 @@ describe('PortfolioCalculator', () => {
date: '2019-08-03', date: '2019-08-03',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
@ -486,6 +511,7 @@ describe('PortfolioCalculator', () => {
date: '2019-09-01', date: '2019-09-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
@ -495,6 +521,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
@ -509,6 +536,7 @@ describe('PortfolioCalculator', () => {
date: '2020-02-02', date: '2020-02-02',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
@ -518,6 +546,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
@ -532,6 +561,7 @@ describe('PortfolioCalculator', () => {
date: '2021-02-01', date: '2021-02-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
@ -541,6 +571,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('15'), quantity: new Big('15'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
@ -555,6 +586,7 @@ describe('PortfolioCalculator', () => {
date: '2021-08-01', date: '2021-08-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
@ -564,6 +596,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('25'), quantity: new Big('25'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
@ -578,7 +611,7 @@ describe('PortfolioCalculator', () => {
}); });
it('with additional buy & sell', () => { it('with additional buy & sell', () => {
const orders = [ const orders: PortfolioOrder[] = [
...ordersVTI, ...ordersVTI,
{ {
date: '2019-09-01', date: '2019-09-01',
@ -588,6 +621,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('2021.99'), unitPrice: new Big('2021.99'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -598,6 +632,7 @@ describe('PortfolioCalculator', () => {
type: OrderType.Sell, type: OrderType.Sell,
unitPrice: new Big('2412.23'), unitPrice: new Big('2412.23'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
} }
]; ];
@ -628,6 +663,7 @@ describe('PortfolioCalculator', () => {
date: '2017-01-03', date: '2017-01-03',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
@ -642,6 +678,7 @@ describe('PortfolioCalculator', () => {
date: '2017-07-01', date: '2017-07-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('0.5614682'), quantity: new Big('0.5614682'),
symbol: 'BTCUSD', symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'), investment: new Big('1999.9999999999998659756'),
@ -651,6 +688,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
@ -665,6 +703,7 @@ describe('PortfolioCalculator', () => {
date: '2018-09-01', date: '2018-09-01',
items: [ items: [
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
@ -674,6 +713,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('0.5614682'), quantity: new Big('0.5614682'),
symbol: 'BTCUSD', symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'), investment: new Big('1999.9999999999998659756'),
@ -683,6 +723,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 1 transactionCount: 1
}, },
{ {
dataSource: DataSource.YAHOO,
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
@ -929,6 +970,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('805.9'), investment: new Big('805.9'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -943,6 +985,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('0'), investment: new Big('0'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -957,6 +1000,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1013.9'), investment: new Big('1013.9'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 3 transactionCount: 3
@ -1005,16 +1049,16 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const transactionPoints = [ const transactionPoints: TransactionPoint[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
quantity: new Big('10'), quantity: new Big('10'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1026,10 +1070,10 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
quantity: new Big('20'), quantity: new Big('20'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -1088,16 +1132,16 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const transactionPoints = [ const transactionPoints: TransactionPoint[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
quantity: new Big('10'), quantity: new Big('10'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(50), fee: new Big(50),
transactionCount: 1 transactionCount: 1
@ -1109,10 +1153,10 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
quantity: new Big('20'), quantity: new Big('20'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(100), fee: new Big(100),
transactionCount: 2 transactionCount: 2
@ -1155,6 +1199,7 @@ describe('PortfolioCalculator', () => {
positions: [ positions: [
{ {
averagePrice: new Big('146.185'), averagePrice: new Big('146.185'),
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
@ -1180,16 +1225,16 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const transactionPoints = [ const transactionPoints: TransactionPoint[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
{ {
quantity: new Big('10'), quantity: new Big('10'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(50), fee: new Big(50),
transactionCount: 1 transactionCount: 1
@ -1201,10 +1246,10 @@ describe('PortfolioCalculator', () => {
items: [ items: [
{ {
quantity: new Big('20'), quantity: new Big('20'),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(100), fee: new Big(100),
transactionCount: 2 transactionCount: 2
@ -1277,6 +1322,7 @@ describe('PortfolioCalculator', () => {
symbol: 'MFA', // Mutual Fund A symbol: 'MFA', // Mutual Fund A
investment: new Big('1000000'), // 1 million investment: new Big('1000000'), // 1 million
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2010-12-31', firstBuyDate: '2010-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1291,6 +1337,7 @@ describe('PortfolioCalculator', () => {
symbol: 'MFA', // Mutual Fund A symbol: 'MFA', // Mutual Fund A
investment: new Big('1100000'), // 1,000,000 + 100,000 investment: new Big('1100000'), // 1,000,000 + 100,000
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2010-12-31', firstBuyDate: '2010-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -1352,6 +1399,7 @@ describe('PortfolioCalculator', () => {
symbol: 'SPA', // Sub Portfolio A symbol: 'SPA', // Sub Portfolio A
investment: new Big('200'), investment: new Big('200'),
currency: Currency.CHF, currency: Currency.CHF,
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31', firstBuyDate: '2012-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1361,6 +1409,7 @@ describe('PortfolioCalculator', () => {
symbol: 'SPB', // Sub Portfolio B symbol: 'SPB', // Sub Portfolio B
investment: new Big('300'), investment: new Big('300'),
currency: Currency.CHF, currency: Currency.CHF,
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31', firstBuyDate: '2012-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1375,6 +1424,7 @@ describe('PortfolioCalculator', () => {
symbol: 'SPA', // Sub Portfolio A symbol: 'SPA', // Sub Portfolio A
investment: new Big('200'), investment: new Big('200'),
currency: Currency.CHF, currency: Currency.CHF,
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31', firstBuyDate: '2012-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1384,6 +1434,7 @@ describe('PortfolioCalculator', () => {
symbol: 'SPB', // Sub Portfolio B symbol: 'SPB', // Sub Portfolio B
investment: new Big('300'), investment: new Big('300'),
currency: Currency.CHF, currency: Currency.CHF,
dataSource: DataSource.YAHOO,
firstBuyDate: '2012-12-31', firstBuyDate: '2012-12-31',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -1488,7 +1539,7 @@ describe('PortfolioCalculator', () => {
currentRateService, currentRateService,
Currency.USD Currency.USD
); );
const transactionPoints = [ const transactionPoints: TransactionPoint[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
@ -1497,6 +1548,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(50), fee: new Big(50),
transactionCount: 1 transactionCount: 1
@ -1511,6 +1563,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(100), fee: new Big(100),
transactionCount: 2 transactionCount: 2
@ -1525,6 +1578,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(150), fee: new Big(150),
transactionCount: 3 transactionCount: 3
@ -1539,6 +1593,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(200), fee: new Big(200),
transactionCount: 4 transactionCount: 4
@ -1553,6 +1608,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(250), fee: new Big(250),
transactionCount: 5 transactionCount: 5
@ -2217,6 +2273,7 @@ describe('PortfolioCalculator', () => {
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2226,6 +2283,7 @@ describe('PortfolioCalculator', () => {
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2334,6 +2392,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('42.97'), unitPrice: new Big('42.97'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2344,6 +2403,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('3562.089535970158'), unitPrice: new Big('3562.089535970158'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2354,6 +2414,7 @@ const ordersMixedSymbols: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('2021.99'), unitPrice: new Big('2021.99'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
} }
]; ];
@ -2367,6 +2428,7 @@ const ordersVTI: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('144.38'), unitPrice: new Big('144.38'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2377,6 +2439,7 @@ const ordersVTI: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('147.99'), unitPrice: new Big('147.99'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2387,6 +2450,7 @@ const ordersVTI: PortfolioOrder[] = [
type: OrderType.Sell, type: OrderType.Sell,
unitPrice: new Big('151.41'), unitPrice: new Big('151.41'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2397,6 +2461,7 @@ const ordersVTI: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('177.69'), unitPrice: new Big('177.69'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
}, },
{ {
@ -2407,6 +2472,7 @@ const ordersVTI: PortfolioOrder[] = [
type: OrderType.Buy, type: OrderType.Buy,
unitPrice: new Big('203.15'), unitPrice: new Big('203.15'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: new Big(0) fee: new Big(0)
} }
]; ];
@ -2420,6 +2486,7 @@ const orderTslaTransactionPoint: TransactionPoint[] = [
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('719.46'), investment: new Big('719.46'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2021-01-01', firstBuyDate: '2021-01-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2437,6 +2504,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2451,6 +2519,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2465,6 +2534,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 3 transactionCount: 3
@ -2479,6 +2549,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 4 transactionCount: 4
@ -2493,6 +2564,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 5 transactionCount: 5
@ -2501,7 +2573,7 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
} }
]; ];
const transactionPointsBuyAndSell = [ const transactionPointsBuyAndSell: TransactionPoint[] = [
{ {
date: '2019-02-01', date: '2019-02-01',
items: [ items: [
@ -2510,6 +2582,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2524,6 +2597,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2538,6 +2612,7 @@ const transactionPointsBuyAndSell = [
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2547,6 +2622,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2561,6 +2637,7 @@ const transactionPointsBuyAndSell = [
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 1 transactionCount: 1
@ -2570,6 +2647,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 3 transactionCount: 3
@ -2584,6 +2662,7 @@ const transactionPointsBuyAndSell = [
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('0'), investment: new Big('0'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2593,6 +2672,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 3 transactionCount: 3
@ -2607,6 +2687,7 @@ const transactionPointsBuyAndSell = [
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('0'), investment: new Big('0'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2616,6 +2697,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 4 transactionCount: 4
@ -2630,6 +2712,7 @@ const transactionPointsBuyAndSell = [
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('0'), investment: new Big('0'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-09-01', firstBuyDate: '2019-09-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 2 transactionCount: 2
@ -2639,6 +2722,7 @@ const transactionPointsBuyAndSell = [
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
currency: Currency.USD, currency: Currency.USD,
dataSource: DataSource.YAHOO,
firstBuyDate: '2019-02-01', firstBuyDate: '2019-02-01',
fee: new Big(0), fee: new Big(0),
transactionCount: 5 transactionCount: 5

View File

@ -1,7 +1,8 @@
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
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 { TimelinePosition } from '@ghostfolio/common/interfaces'; import { TimelinePosition } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
addDays, addDays,
@ -59,6 +60,7 @@ export class PortfolioCalculator {
.plus(oldAccumulatedSymbol.quantity); .plus(oldAccumulatedSymbol.quantity);
currentTransactionPointItem = { currentTransactionPointItem = {
currency: order.currency, currency: order.currency,
dataSource: order.dataSource,
fee: order.fee.plus(oldAccumulatedSymbol.fee), fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
investment: newQuantity.eq(0) investment: newQuantity.eq(0)
@ -74,6 +76,7 @@ export class PortfolioCalculator {
} else { } else {
currentTransactionPointItem = { currentTransactionPointItem = {
currency: order.currency, currency: order.currency,
dataSource: order.dataSource,
fee: order.fee, fee: order.fee,
firstBuyDate: order.date, firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor), investment: unitPrice.mul(order.quantity).mul(factor),
@ -153,12 +156,15 @@ export class PortfolioCalculator {
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let firstIndex = this.transactionPoints.length; let firstIndex = this.transactionPoints.length;
const dates = []; const dates = [];
const symbols = new Set<string>(); const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: Currency } = {}; const currencies: { [symbol: string]: Currency } = {};
dates.push(resetHours(start)); dates.push(resetHours(start));
for (const item of this.transactionPoints[firstIndex - 1].items) { for (const item of this.transactionPoints[firstIndex - 1].items) {
symbols.add(item.symbol); dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency; currencies[item.symbol] = item.currency;
} }
for (let i = 0; i < this.transactionPoints.length; i++) { for (let i = 0; i < this.transactionPoints.length; i++) {
@ -178,10 +184,10 @@ export class PortfolioCalculator {
const marketSymbols = await this.currentRateService.getValues({ const marketSymbols = await this.currentRateService.getValues({
currencies, currencies,
dataGatheringItems,
dateQuery: { dateQuery: {
in: dates in: dates
}, },
symbols: Array.from(symbols),
userCurrency: this.currency userCurrency: this.currency
}); });
@ -309,6 +315,7 @@ export class PortfolioCalculator {
? new Big(0) ? new Big(0)
: item.investment.div(item.quantity), : item.investment.div(item.quantity),
currency: item.currency, currency: item.currency,
dataSource: item.dataSource,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: isValid grossPerformance: isValid
? grossPerformance[item.symbol] ?? null ? grossPerformance[item.symbol] ?? null
@ -515,25 +522,28 @@ export class PortfolioCalculator {
} = {}; } = {};
if (j >= 0) { if (j >= 0) {
const currencies: { [name: string]: Currency } = {}; const currencies: { [name: string]: Currency } = {};
const symbols: string[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
for (const item of this.transactionPoints[j].items) { for (const item of this.transactionPoints[j].items) {
currencies[item.symbol] = item.currency; currencies[item.symbol] = item.currency;
symbols.push(item.symbol); dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
investment = investment.add(item.investment); investment = investment.add(item.investment);
fees = fees.add(item.fee); fees = fees.add(item.fee);
} }
let marketSymbols: GetValueObject[] = []; let marketSymbols: GetValueObject[] = [];
if (symbols.length > 0) { if (dataGatheringItems.length > 0) {
try { try {
marketSymbols = await this.currentRateService.getValues({ marketSymbols = await this.currentRateService.getValues({
currencies,
dataGatheringItems,
dateQuery: { dateQuery: {
gte: startDate, gte: startDate,
lt: endOfDay(endDate) lt: endOfDay(endDate)
}, },
symbols,
currencies,
userCurrency: this.currency userCurrency: this.currency
}); });
} catch (error) { } catch (error) {

View File

@ -191,12 +191,18 @@ export class PortfolioService {
); );
const totalValue = currentPositions.currentValue.plus(cashDetails.balance); const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
const dataGatheringItems = currentPositions.positions.map((position) => {
return {
dataSource: position.dataSource,
symbol: position.symbol
};
});
const symbols = currentPositions.positions.map( const symbols = currentPositions.positions.map(
(position) => position.symbol (position) => position.symbol
); );
const [dataProviderResponses, symbolProfiles] = await Promise.all([ const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.get(symbols), this.dataProviderService.get(dataGatheringItems),
this.symbolProfileService.getSymbolProfiles(symbols) this.symbolProfileService.getSymbolProfiles(symbols)
]); ]);
@ -297,6 +303,7 @@ export class PortfolioService {
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee), fee: new Big(order.fee),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
@ -326,6 +333,7 @@ export class PortfolioService {
const { const {
averagePrice, averagePrice,
currency, currency,
dataSource,
firstBuyDate, firstBuyDate,
marketPrice, marketPrice,
quantity, quantity,
@ -351,7 +359,7 @@ export class PortfolioService {
); );
const historicalData = await this.dataProviderService.getHistorical( const historicalData = await this.dataProviderService.getHistorical(
[aSymbol], [{ dataSource, symbol: aSymbol }],
'day', 'day',
parseISO(firstBuyDate), parseISO(firstBuyDate),
new Date() new Date()
@ -421,11 +429,13 @@ export class PortfolioService {
symbol: aSymbol symbol: aSymbol
}; };
} else { } else {
const currentData = await this.dataProviderService.get([aSymbol]); const currentData = await this.dataProviderService.get([
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
]);
const marketPrice = currentData[aSymbol]?.marketPrice; const marketPrice = currentData[aSymbol]?.marketPrice;
let historicalData = await this.dataProviderService.getHistorical( let historicalData = await this.dataProviderService.getHistorical(
[aSymbol], [{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
'day', 'day',
portfolioStart, portfolioStart,
new Date() new Date()
@ -507,10 +517,16 @@ export class PortfolioService {
const positions = currentPositions.positions.filter( const positions = currentPositions.positions.filter(
(item) => !item.quantity.eq(0) (item) => !item.quantity.eq(0)
); );
const dataGatheringItem = positions.map((position) => {
return {
dataSource: position.dataSource,
symbol: position.symbol
};
});
const symbols = positions.map((position) => position.symbol); const symbols = positions.map((position) => position.symbol);
const [dataProviderResponses, symbolProfiles] = await Promise.all([ const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.get(symbols), this.dataProviderService.get(dataGatheringItem),
this.symbolProfileService.getSymbolProfiles(symbols) this.symbolProfileService.getSymbolProfiles(symbols)
]); ]);
@ -813,6 +829,7 @@ export class PortfolioService {
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big( fee: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(

View File

@ -10,6 +10,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@ -46,10 +47,20 @@ export class SymbolController {
/** /**
* Must be after /lookup * Must be after /lookup
*/ */
@Get(':symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> { public async getSymbolData(
const result = await this.symbolService.get(symbol); @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<SymbolItem> {
if (!DataSource[dataSource]) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
const result = await this.symbolService.get({ dataSource, symbol });
if (!result || isEmpty(result)) { if (!result || isEmpty(result)) {
throw new HttpException( throw new HttpException(

View File

@ -1,4 +1,5 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency, DataSource } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
@ -13,15 +14,15 @@ export class SymbolService {
private readonly prismaService: PrismaService private readonly prismaService: PrismaService
) {} ) {}
public async get(aSymbol: string): Promise<SymbolItem> { public async get(dataGatheringItem: IDataGatheringItem): Promise<SymbolItem> {
const response = await this.dataProviderService.get([aSymbol]); const response = await this.dataProviderService.get([dataGatheringItem]);
const { currency, dataSource, marketPrice } = response[aSymbol] ?? {}; const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {};
if (dataSource && marketPrice) { if (dataGatheringItem.dataSource && marketPrice) {
return { return {
dataSource,
marketPrice, marketPrice,
currency: <Currency>(<unknown>currency) currency: <Currency>(<unknown>currency),
dataSource: dataGatheringItem.dataSource
}; };
} }

View File

@ -3,17 +3,11 @@ import {
currencyPairs, currencyPairs,
ghostfolioFearAndGreedIndexSymbol ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
DATE_FORMAT,
getUtc,
isGhostfolioScraperApiSymbol,
resetHours
} from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { import {
differenceInHours, differenceInHours,
endOfToday,
format, format,
getDate, getDate,
getMonth, getMonth,
@ -123,20 +117,17 @@ export class DataGatheringService {
} }
} }
public async gatherProfileData(aSymbols?: string[]) { public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) {
console.log('Profile data gathering has been started.'); console.log('Profile data gathering has been started.');
console.time('data-gathering-profile'); console.time('data-gathering-profile');
let symbols = aSymbols; let dataGatheringItems = aDataGatheringItems;
if (!symbols) { if (!dataGatheringItems) {
const dataGatheringItems = await this.getSymbolsProfileData(); dataGatheringItems = await this.getSymbolsProfileData();
symbols = dataGatheringItems.map((dataGatheringItem) => {
return dataGatheringItem.symbol;
});
} }
const currentData = await this.dataProviderService.get(symbols); const currentData = await this.dataProviderService.get(dataGatheringItems);
for (const [ for (const [
symbol, symbol,
@ -215,6 +206,7 @@ export class DataGatheringService {
try { try {
await this.prismaService.marketData.create({ await this.prismaService.marketData.create({
data: { data: {
dataSource,
symbol, symbol,
date: currentDate, date: currentDate,
marketPrice: lastMarketPrice marketPrice: lastMarketPrice

View File

@ -6,11 +6,7 @@ import {
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { import { DATE_FORMAT } from '@ghostfolio/common/helper';
DATE_FORMAT,
isGhostfolioScraperApiSymbol,
isRakutenRapidApiSymbol
} from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
@ -20,8 +16,8 @@ import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service'; import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
import { import {
convertToYahooFinanceSymbol, YahooFinanceService,
YahooFinanceService convertToYahooFinanceSymbol
} from './yahoo-finance/yahoo-finance.service'; } from './yahoo-finance/yahoo-finance.service';
@Injectable() @Injectable()
@ -37,53 +33,32 @@ export class DataProviderService {
this.rakutenRapidApiService?.setPrisma(this.prismaService); this.rakutenRapidApiService?.setPrisma(this.prismaService);
} }
public async get( public async get(items: IDataGatheringItem[]): Promise<{
aSymbols: string[] [symbol: string]: IDataProviderResponse;
): Promise<{ [symbol: string]: IDataProviderResponse }> { }> {
if (aSymbols.length === 1) { const response: {
const symbol = aSymbols[0]; [symbol: string]: IDataProviderResponse;
} = {};
if (isGhostfolioScraperApiSymbol(symbol)) { for (const item of items) {
return this.ghostfolioScraperApiService.get(aSymbols); if (item.dataSource === DataSource.ALPHA_VANTAGE) {
} else if (isRakutenRapidApiSymbol(symbol)) { response[item.symbol] = (
return this.rakutenRapidApiService.get(aSymbols); await this.alphaVantageService.get([item.symbol])
} )[item.symbol];
} } else if (item.dataSource === DataSource.GHOSTFOLIO) {
response[item.symbol] = (
const yahooFinanceSymbols = aSymbols await this.ghostfolioScraperApiService.get([item.symbol])
.filter((symbol) => { )[item.symbol];
return ( } else if (item.dataSource === DataSource.RAKUTEN) {
!isGhostfolioScraperApiSymbol(symbol) && response[item.symbol] = (
!isRakutenRapidApiSymbol(symbol) await this.rakutenRapidApiService.get([item.symbol])
); )[item.symbol];
}) } else if (item.dataSource === DataSource.YAHOO) {
.map((symbol) => { response[item.symbol] = (
return convertToYahooFinanceSymbol(symbol); await this.yahooFinanceService.get([
}); convertToYahooFinanceSymbol(item.symbol)
])
const response = await this.yahooFinanceService.get(yahooFinanceSymbols); )[item.symbol];
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
return isGhostfolioScraperApiSymbol(symbol);
});
for (const symbol of ghostfolioScraperApiSymbols) {
if (symbol) {
const ghostfolioScraperApiResult =
await this.ghostfolioScraperApiService.get([symbol]);
response[symbol] = ghostfolioScraperApiResult[symbol];
}
}
const rakutenRapidApiSymbols = aSymbols.filter((symbol) => {
return isRakutenRapidApiSymbol(symbol);
});
for (const symbol of rakutenRapidApiSymbols) {
if (symbol) {
const rakutenRapidApiResult =
await this.ghostfolioScraperApiService.get([symbol]);
response[symbol] = rakutenRapidApiResult[symbol];
} }
} }
@ -91,7 +66,7 @@ export class DataProviderService {
} }
public async getHistorical( public async getHistorical(
aSymbols: string[], aItems: IDataGatheringItem[],
aGranularity: Granularity = 'month', aGranularity: Granularity = 'month',
from: Date, from: Date,
to: Date to: Date
@ -115,8 +90,17 @@ export class DataProviderService {
)}'` )}'`
: ''; : '';
const dataSources = aItems.map((item) => {
return item.dataSource;
});
const symbols = aItems.map((item) => {
return item.symbol;
});
try { try {
const queryRaw = `SELECT * FROM "MarketData" WHERE "symbol" IN ('${aSymbols.join( const queryRaw = `SELECT * FROM "MarketData" WHERE "dataSource" IN ('${dataSources.join(
`','`
)}') AND "symbol" IN ('${symbols.join(
`','` `','`
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`; )}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
@ -175,13 +159,24 @@ export class DataProviderService {
} }
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
const { items } = await this.getDataProvider( const promises: Promise<{ items: LookupItem[] }>[] = [];
<DataSource>this.configurationService.get('DATA_SOURCES')[0] let lookupItems: LookupItem[] = [];
).search(aSymbol);
const filteredItems = items.filter((item) => { for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
promises.push(
this.getDataProvider(DataSource[dataSource]).search(aSymbol)
);
}
const searchResults = await Promise.all(promises);
searchResults.forEach((searchResult) => {
lookupItems = lookupItems.concat(searchResult.items);
});
const filteredItems = lookupItems.filter((lookupItem) => {
// Only allow symbols with supported currency // Only allow symbols with supported currency
return item.currency ? true : false; return lookupItem.currency ? true : false;
}); });
return { return {

View File

@ -94,6 +94,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
await this.prismaService.marketData.create({ await this.prismaService.marketData.create({
data: { data: {
symbol, symbol,
dataSource: DataSource.RAKUTEN,
date: subWeeks(getToday(), 1), date: subWeeks(getToday(), 1),
marketPrice: fgi.oneWeekAgo.value marketPrice: fgi.oneWeekAgo.value
} }
@ -102,6 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
await this.prismaService.marketData.create({ await this.prismaService.marketData.create({
data: { data: {
symbol, symbol,
dataSource: DataSource.RAKUTEN,
date: subMonths(getToday(), 1), date: subMonths(getToday(), 1),
marketPrice: fgi.oneMonthAgo.value marketPrice: fgi.oneMonthAgo.value
} }
@ -110,6 +112,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
await this.prismaService.marketData.create({ await this.prismaService.marketData.create({
data: { data: {
symbol, symbol,
dataSource: DataSource.RAKUTEN,
date: subYears(getToday(), 1), date: subYears(getToday(), 1),
marketPrice: fgi.oneYearAgo.value marketPrice: fgi.oneYearAgo.value
} }

View File

@ -1,15 +1,16 @@
import { currencyPairs } from '@ghostfolio/common/config'; import { currencyPairs } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { isEmpty, isNumber } from 'lodash'; import { isEmpty, isNumber } from 'lodash';
import { DataProviderService } from './data-provider/data-provider.service'; import { DataProviderService } from './data-provider/data-provider.service';
import { IDataGatheringItem } from './interfaces/interfaces';
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencyPairs: string[] = []; private currencyPairs: IDataGatheringItem[] = [];
private exchangeRates: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor(private dataProviderService: DataProviderService) { public constructor(private dataProviderService: DataProviderService) {
@ -20,8 +21,8 @@ export class ExchangeRateDataService {
this.currencyPairs = []; this.currencyPairs = [];
this.exchangeRates = {}; this.exchangeRates = {};
for (const { currency1, currency2 } of currencyPairs) { for (const { currency1, currency2, dataSource } of currencyPairs) {
this.addCurrencyPairs(currency1, currency2); this.addCurrencyPairs({ currency1, currency2, dataSource });
} }
await this.loadCurrencies(); await this.loadCurrencies();
@ -39,8 +40,8 @@ export class ExchangeRateDataService {
// Load currencies directly from data provider as a fallback // Load currencies directly from data provider as a fallback
// if historical data is not yet available // if historical data is not yet available
const historicalData = await this.dataProviderService.get( const historicalData = await this.dataProviderService.get(
this.currencyPairs.map((currencyPair) => { this.currencyPairs.map(({ dataSource, symbol }) => {
return currencyPair; return { dataSource, symbol };
}) })
); );
@ -67,21 +68,21 @@ export class ExchangeRateDataService {
}; };
}); });
this.currencyPairs.forEach((pair) => { this.currencyPairs.forEach(({ symbol }) => {
const [currency1, currency2] = pair.match(/.{1,3}/g); const [currency1, currency2] = symbol.match(/.{1,3}/g);
const date = format(getYesterday(), DATE_FORMAT); const date = format(getYesterday(), DATE_FORMAT);
this.exchangeRates[pair] = resultExtended[pair]?.[date]?.marketPrice; this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
if (!this.exchangeRates[pair]) { if (!this.exchangeRates[symbol]) {
// Not found, calculate indirectly via USD // Not found, calculate indirectly via USD
this.exchangeRates[pair] = this.exchangeRates[symbol] =
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice * resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice; resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
// Calculate the opposite direction // Calculate the opposite direction
this.exchangeRates[`${currency2}${currency1}`] = this.exchangeRates[`${currency2}${currency1}`] =
1 / this.exchangeRates[pair]; 1 / this.exchangeRates[symbol];
} }
}); });
} }
@ -123,8 +124,22 @@ export class ExchangeRateDataService {
return aValue; return aValue;
} }
private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) { private addCurrencyPairs({
this.currencyPairs.push(`${aCurrency1}${aCurrency2}`); currency1,
this.currencyPairs.push(`${aCurrency2}${aCurrency1}`); currency2,
dataSource
}: {
currency1: Currency;
currency2: Currency;
dataSource: DataSource;
}) {
this.currencyPairs.push({
dataSource,
symbol: `${currency1}${currency2}`
});
this.currencyPairs.push({
dataSource,
symbol: `${currency2}${currency1}`
});
} }
} }

View File

@ -29,6 +29,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -112,7 +113,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
if (this.hasPermissionToAccessFearAndGreedIndex) { if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService this.dataService
.fetchSymbolItem(ghostfolioFearAndGreedIndexSymbol) .fetchSymbolItem({
dataSource: DataSource.RAKUTEN,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
this.fearAndGreedIndex = marketPrice; this.fearAndGreedIndex = marketPrice;

View File

@ -3,7 +3,8 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Inject, Inject,
OnDestroy OnDestroy,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormControl, Validators } from '@angular/forms'; import { FormControl, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
@ -11,6 +12,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { isString } from 'lodash';
import { EMPTY, Observable, Subject } from 'rxjs'; import { EMPTY, Observable, Subject } from 'rxjs';
import { import {
catchError, catchError,
@ -31,13 +33,18 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
templateUrl: 'create-or-update-transaction-dialog.html' templateUrl: 'create-or-update-transaction-dialog.html'
}) })
export class CreateOrUpdateTransactionDialog implements OnDestroy { export class CreateOrUpdateTransactionDialog implements OnDestroy {
@ViewChild('autocomplete') autocomplete;
public currencies: Currency[] = []; public currencies: Currency[] = [];
public currentMarketPrice = null; public currentMarketPrice = null;
public filteredLookupItems: Observable<LookupItem[]>; public filteredLookupItems: Observable<LookupItem[]>;
public isLoading = false; public isLoading = false;
public platforms: { id: string; name: string }[]; public platforms: { id: string; name: string }[];
public searchSymbolCtrl = new FormControl( public searchSymbolCtrl = new FormControl(
this.data.transaction.symbol, {
dataSource: this.data.transaction.dataSource,
name: this.data.transaction.symbol
},
Validators.required Validators.required
); );
@ -60,9 +67,9 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
startWith(''), startWith(''),
debounceTime(400), debounceTime(400),
distinctUntilChanged(), distinctUntilChanged(),
switchMap((aQuery: string) => { switchMap((query: string) => {
if (aQuery) { if (isString(query)) {
return this.dataService.fetchSymbols(aQuery); return this.dataService.fetchSymbols(query);
} }
return []; return [];
@ -71,7 +78,10 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
if (this.data.transaction.symbol) { if (this.data.transaction.symbol) {
this.dataService this.dataService
.fetchSymbolItem(this.data.transaction.symbol) .fetchSymbolItem({
dataSource: this.data.transaction.dataSource,
symbol: this.data.transaction.symbol
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
this.currentMarketPrice = marketPrice; this.currentMarketPrice = marketPrice;
@ -85,9 +95,21 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.data.transaction.unitPrice = this.currentMarketPrice; this.data.transaction.unitPrice = this.currentMarketPrice;
} }
public displayFn(aLookupItem: LookupItem) {
return aLookupItem?.name ?? '';
}
public onBlurSymbol() { public onBlurSymbol() {
const symbol = this.searchSymbolCtrl.value; this.data.transaction.currency = null;
this.updateSymbol(symbol); this.data.transaction.dataSource = null;
if (this.autocomplete.isOpen) {
this.searchSymbolCtrl.setErrors({ incorrect: true });
} else {
this.data.transaction.unitPrice = null;
}
this.changeDetectorRef.markForCheck();
} }
public onCancel(): void { public onCancel(): void {
@ -95,7 +117,8 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
} }
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) { public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.updateSymbol(event.option.value); this.data.transaction.dataSource = event.option.value.dataSource;
this.updateSymbol(event.option.value.symbol);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -106,10 +129,15 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
private updateSymbol(symbol: string) { private updateSymbol(symbol: string) {
this.isLoading = true; this.isLoading = true;
this.searchSymbolCtrl.setErrors(null);
this.data.transaction.symbol = symbol; this.data.transaction.symbol = symbol;
this.dataService this.dataService
.fetchSymbolItem(this.data.transaction.symbol) .fetchSymbolItem({
dataSource: this.data.transaction.dataSource,
symbol: this.data.transaction.symbol
})
.pipe( .pipe(
catchError(() => { catchError(() => {
this.data.transaction.currency = null; this.data.transaction.currency = null;

View File

@ -28,18 +28,19 @@
matInput matInput
required required
[formControl]="searchSymbolCtrl" [formControl]="searchSymbolCtrl"
[matAutocomplete]="auto" [matAutocomplete]="autocomplete"
(blur)="onBlurSymbol()" (blur)="onBlurSymbol()"
/> />
<mat-autocomplete <mat-autocomplete
#auto="matAutocomplete" #autocomplete="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="onUpdateSymbol($event)" (optionSelected)="onUpdateSymbol($event)"
> >
<ng-container> <ng-container>
<mat-option <mat-option
*ngFor="let lookupItem of filteredLookupItems | async" *ngFor="let lookupItem of filteredLookupItems | async"
class="autocomplete" class="autocomplete"
[value]="lookupItem.symbol" [value]="lookupItem"
> >
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span <span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
><span><b>{{ lookupItem.name }}</b></span> ><span><b>{{ lookupItem.name }}</b></span>

View File

@ -29,8 +29,11 @@ import {
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { Order as OrderModel } from '@prisma/client'; import {
import { Account as AccountModel } from '@prisma/client'; Account as AccountModel,
DataSource,
Order as OrderModel
} from '@prisma/client';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -108,8 +111,14 @@ export class DataService {
return info; return info;
} }
public fetchSymbolItem(aSymbol: string) { public fetchSymbolItem({
return this.http.get<SymbolItem>(`/api/symbol/${aSymbol}`); dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`);
} }
public fetchPositions({ public fetchPositions({

View File

@ -1,9 +1,10 @@
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
export interface TimelinePosition { export interface TimelinePosition {
averagePrice: Big; averagePrice: Big;
currency: Currency; currency: Currency;
dataSource: DataSource;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';

View File

@ -61,9 +61,10 @@ model AuthDevice {
} }
model MarketData { model MarketData {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
dataSource DataSource @default(YAHOO)
date DateTime date DateTime
id String @default(uuid()) id String @default(uuid())
symbol String symbol String
marketPrice Float marketPrice Float