Browse Source

Merge pull request #88 from dandevaud/bugfix/fix-marketdata-update

Bugfix/fix marketdata update
pull/5027/head
dandevaud 1 year ago
committed by GitHub
parent
commit
9cec38c1f4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 176
      apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts
  2. 2
      apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
  3. 6
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  4. 51
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 5
      apps/api/src/services/data-gathering/data-gathering.service.ts
  6. 10
      apps/api/src/services/market-data/market-data.service.ts
  7. 34
      yarn.lock

176
apps/api/src/app/portfolio/calculator/constantPortfolioReturn/portfolio-calculator.ts

@ -1,14 +1,25 @@
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { import {
HistoricalDataItem, getFactor,
SymbolMetrics, getInterval
UniqueAsset } from '@ghostfolio/api/helper/portfolio.helper';
} from '@ghostfolio/common/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { MAX_CHART_ITEMS } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addDays, eachDayOfInterval, format } from 'date-fns'; import {
addDays,
differenceInDays,
eachDayOfInterval,
format,
isAfter,
isBefore,
isEqual,
subDays
} from 'date-fns';
import { PortfolioOrder } from '../../interfaces/portfolio-order.interface'; import { PortfolioOrder } from '../../interfaces/portfolio-order.interface';
import { TWRPortfolioCalculator } from '../twr/portfolio-calculator'; import { TWRPortfolioCalculator } from '../twr/portfolio-calculator';
@ -17,44 +28,58 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
private holdings: { [date: string]: { [symbol: string]: Big } } = {}; private holdings: { [date: string]: { [symbol: string]: Big } } = {};
private holdingCurrencies: { [symbol: string]: string } = {}; private holdingCurrencies: { [symbol: string]: string } = {};
protected calculateOverallPerformance( @LogPerformance
positions: TimelinePosition[] public async getChart({
): PortfolioSnapshot { dateRange = 'max',
return super.calculateOverallPerformance(positions); withDataDecimation = true,
} withTimeWeightedReturn = false
protected getSymbolMetrics({
dataSource,
end,
exchangeRates,
isChartMode = false,
marketSymbolMap,
start,
step = 1,
symbol
}: { }: {
end: Date; dateRange?: DateRange;
exchangeRates: { [dateString: string]: number }; withDataDecimation?: boolean;
isChartMode?: boolean; withTimeWeightedReturn?: boolean;
marketSymbolMap: { }): Promise<HistoricalDataItem[]> {
[date: string]: { [symbol: string]: Big }; const { endDate, startDate } = getInterval(dateRange, this.getStartDate());
};
start: Date; const daysInMarket = differenceInDays(endDate, startDate) + 1;
step?: number; const step = withDataDecimation
} & UniqueAsset): SymbolMetrics { ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS))
return super.getSymbolMetrics({ : 1;
dataSource,
end, let item = super.getChartData({
exchangeRates, step,
isChartMode, end: endDate,
marketSymbolMap, start: startDate
start, });
if (!withTimeWeightedReturn) {
return item;
} else {
let timeWeighted = await this.getTimeWeightedChartData({
step, step,
symbol end: endDate,
start: startDate
});
return item.then((data) => {
return data.map((item) => {
let timeWeightedItem = timeWeighted.find(
(timeWeightedItem) => timeWeightedItem.date === item.date
);
if (timeWeightedItem) {
item.timeWeightedPerformance =
timeWeightedItem.netPerformanceInPercentage;
item.timeWeightedPerformanceWithCurrencyEffect =
timeWeightedItem.netPerformanceInPercentageWithCurrencyEffect;
}
return item;
}); });
});
}
} }
public override async getChartData({ @LogPerformance
private async getTimeWeightedChartData({
end = new Date(Date.now()), end = new Date(Date.now()),
start, start,
step = 1 step = 1
@ -63,13 +88,18 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
start: Date; start: Date;
step?: number; step?: number;
}): Promise<HistoricalDataItem[]> { }): Promise<HistoricalDataItem[]> {
const timelineHoldings = this.getHoldings(start, end); let marketMapTask = this.computeMarketMap({ in: [start, end] });
const timelineHoldings = await this.getHoldings(start, end);
const calculationDates = Object.keys(timelineHoldings) const calculationDates = Object.keys(timelineHoldings)
.filter((date) => { .filter((date) => {
let parsed = parseDate(date); let parsed = parseDate(date);
parsed >= start && parsed <= end; return (
isAfter(parsed, subDays(start, 1)) &&
isBefore(parsed, addDays(end, 1))
);
}) })
.sort(); .sort((a, b) => parseDate(a).getTime() - parseDate(b).getTime());
let data: HistoricalDataItem[] = []; let data: HistoricalDataItem[] = [];
const startString = format(start, DATE_FORMAT); const startString = format(start, DATE_FORMAT);
@ -88,11 +118,13 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
await marketMapTask;
let totalInvestment = Object.keys(timelineHoldings[startString]).reduce( let totalInvestment = Object.keys(timelineHoldings[startString]).reduce(
(sum, holding) => { (sum, holding) => {
return sum.plus( return sum.plus(
timelineHoldings[startString][holding].mul( timelineHoldings[startString][holding].mul(
this.marketMap[startString][holding] this.marketMap[startString][holding] ?? new Big(0)
) )
); );
}, },
@ -149,6 +181,7 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
return data; return data;
} }
@LogPerformance
private async handleSingleHolding( private async handleSingleHolding(
previousDate: string, previousDate: string,
holding: string, holding: string,
@ -203,6 +236,7 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
}; };
} }
@LogPerformance
private getCurrency(symbol: string) { private getCurrency(symbol: string) {
if (!this.holdingCurrencies[symbol]) { if (!this.holdingCurrencies[symbol]) {
this.holdingCurrencies[symbol] = this.activities.find( this.holdingCurrencies[symbol] = this.activities.find(
@ -213,11 +247,16 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
return this.holdingCurrencies[symbol]; return this.holdingCurrencies[symbol];
} }
private getHoldings(start: Date, end: Date) { @LogPerformance
private async getHoldings(start: Date, end: Date) {
if ( if (
this.holdings && this.holdings &&
Object.keys(this.holdings).some((h) => parseDate(h) >= end) && Object.keys(this.holdings).some((h) =>
Object.keys(this.holdings).some((h) => parseDate(h) <= start) isAfter(parseDate(h), subDays(end, 1))
) &&
Object.keys(this.holdings).some((h) =>
isBefore(parseDate(h), addDays(start, 1))
)
) { ) {
return this.holdings; return this.holdings;
} }
@ -226,7 +265,8 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
return this.holdings; return this.holdings;
} }
private computeHoldings(start: Date, end: Date) { @LogPerformance
private async computeHoldings(start: Date, end: Date) {
const investmentByDate = this.getInvestmentByDate(); const investmentByDate = this.getInvestmentByDate();
const transactionDates = Object.keys(investmentByDate).sort(); const transactionDates = Object.keys(investmentByDate).sort();
let dates = eachDayOfInterval({ start, end }, { step: 1 }) let dates = eachDayOfInterval({ start, end }, { step: 1 })
@ -258,6 +298,7 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
this.holdings = currentHoldings; this.holdings = currentHoldings;
} }
@LogPerformance
private calculateInitialHoldings( private calculateInitialHoldings(
investmentByDate: { [date: string]: PortfolioOrder[] }, investmentByDate: { [date: string]: PortfolioOrder[] },
start: Date, start: Date,
@ -288,6 +329,7 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
} }
} }
@LogPerformance
private getInvestmentByDate(): { [date: string]: PortfolioOrder[] } { private getInvestmentByDate(): { [date: string]: PortfolioOrder[] } {
return this.activities.reduce((groupedByDate, order) => { return this.activities.reduce((groupedByDate, order) => {
if (!groupedByDate[order.date]) { if (!groupedByDate[order.date]) {
@ -299,4 +341,40 @@ export class CPRPortfolioCalculator extends TWRPortfolioCalculator {
return groupedByDate; return groupedByDate;
}, {}); }, {});
} }
@LogPerformance
private async computeMarketMap(dateQuery: { in: Date[] }) {
const dataGatheringItems: IDataGatheringItem[] = this.activities.map(
(activity) => {
return {
symbol: activity.SymbolProfile.symbol,
dataSource: activity.SymbolProfile.dataSource
};
}
);
const { values: marketSymbols } = await this.currentRateService.getValues({
dataGatheringItems,
dateQuery
});
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
for (const marketSymbol of marketSymbols) {
const date = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[date]) {
marketSymbolMap[date] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
this.marketMap = marketSymbolMap;
}
} }

2
apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts

@ -64,7 +64,7 @@ export class PortfolioCalculatorFactory {
redisCacheService: this.redisCacheService redisCacheService: this.redisCacheService
}); });
case PerformanceCalculationType.TWR: case PerformanceCalculationType.TWR:
return new TWRPortfolioCalculator({ return new CPRPortfolioCalculator({
accountBalanceItems, accountBalanceItems,
activities, activities,
currency, currency,

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

@ -54,7 +54,7 @@ export abstract class PortfolioCalculator {
private configurationService: ConfigurationService; private configurationService: ConfigurationService;
protected currency: string; protected currency: string;
private currentRateService: CurrentRateService; protected currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[]; private dataProviderInfos: DataProviderInfo[];
private dateRange: DateRange; private dateRange: DateRange;
private endDate: Date; private endDate: Date;
@ -424,10 +424,12 @@ export abstract class PortfolioCalculator {
public async getChart({ public async getChart({
dateRange = 'max', dateRange = 'max',
withDataDecimation = true withDataDecimation = true,
withTimeWeightedReturn = false
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
withDataDecimation?: boolean; withDataDecimation?: boolean;
withTimeWeightedReturn?: boolean;
}): Promise<HistoricalDataItem[]> { }): Promise<HistoricalDataItem[]> {
const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); const { endDate, startDate } = getInterval(dateRange, this.getStartDate());

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

@ -1282,18 +1282,9 @@ export class PortfolioService {
let currentNetWorth = 0; let currentNetWorth = 0;
let items = await portfolioCalculator.getChart({ let items = await portfolioCalculator.getChart({
dateRange
});
items = await this.calculatedTimeWeightedPerformance(
calculateTimeWeightedPerformance,
activities,
dateRange, dateRange,
userId, withTimeWeightedReturn: calculateTimeWeightedPerformance
userCurrency, });
filters,
items
);
const itemOfToday = items.find(({ date }) => { const itemOfToday = items.find(({ date }) => {
return date === format(new Date(), DATE_FORMAT); return date === format(new Date(), DATE_FORMAT);
@ -1342,44 +1333,6 @@ export class PortfolioService {
}; };
} }
private async calculatedTimeWeightedPerformance(
calculateTimeWeightedPerformance: boolean,
activities: Activity[],
dateRange: string,
userId: string,
userCurrency: string,
filters: Filter[],
items: HistoricalDataItem[]
) {
if (calculateTimeWeightedPerformance) {
const portfolioCalculatorCPR = this.calculatorFactory.createCalculator({
activities,
dateRange,
userId,
calculationType: PerformanceCalculationType.CPR,
currency: userCurrency,
hasFilters: filters?.length > 0,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
});
let timeWeightedInvestmentItems = await portfolioCalculatorCPR.getChart({
dateRange
});
items = items.map((item) => {
let matchingItem = timeWeightedInvestmentItems.find(
(timeWeightedInvestmentItem) =>
timeWeightedInvestmentItem.date === item.date
);
item.timeWeightedPerformance = matchingItem.netPerformanceInPercentage;
item.timeWeightedPerformanceWithCurrencyEffect =
matchingItem.netPerformanceInPercentageWithCurrencyEffect;
return item;
});
}
return items;
}
@LogPerformance @LogPerformance
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);

5
apps/api/src/services/data-gathering/data-gathering.service.ts

@ -117,8 +117,6 @@ export class DataGatheringService {
historicalData[symbol][format(date, DATE_FORMAT)].marketPrice; historicalData[symbol][format(date, DATE_FORMAT)].marketPrice;
if (marketPrice) { if (marketPrice) {
await this.lock.acquireAsync();
try {
return await this.prismaService.marketData.upsert({ return await this.prismaService.marketData.upsert({
create: { create: {
dataSource, dataSource,
@ -129,9 +127,6 @@ export class DataGatheringService {
update: { marketPrice }, update: { marketPrice },
where: { dataSource_date_symbol: { dataSource, date, symbol } } where: { dataSource_date_symbol: { dataSource, date, symbol } }
}); });
} finally {
this.lock.release();
}
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataGatheringService'); Logger.error(error, 'DataGatheringService');

10
apps/api/src/services/market-data/market-data.service.ts

@ -124,8 +124,6 @@ export class MarketDataService {
where: Prisma.MarketDataWhereUniqueInput; where: Prisma.MarketDataWhereUniqueInput;
}): Promise<MarketData> { }): Promise<MarketData> {
const { data, where } = params; const { data, where } = params;
await this.lock.acquireAsync();
try {
return this.prismaService.marketData.upsert({ return this.prismaService.marketData.upsert({
where, where,
create: { create: {
@ -137,9 +135,6 @@ export class MarketDataService {
}, },
update: { marketPrice: data.marketPrice, state: data.state } update: { marketPrice: data.marketPrice, state: data.state }
}); });
} finally {
this.lock.release();
}
} }
/** /**
@ -153,8 +148,6 @@ export class MarketDataService {
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
const upsertPromises = data.map( const upsertPromises = data.map(
async ({ dataSource, date, marketPrice, symbol, state }) => { async ({ dataSource, date, marketPrice, symbol, state }) => {
await this.lock.acquireAsync();
try {
return this.prismaService.marketData.upsert({ return this.prismaService.marketData.upsert({
create: { create: {
dataSource: <DataSource>dataSource, dataSource: <DataSource>dataSource,
@ -175,9 +168,6 @@ export class MarketDataService {
} }
} }
}); });
} finally {
this.lock.release();
}
} }
); );
return await Promise.all(upsertPromises); return await Promise.all(upsertPromises);

34
yarn.lock

@ -13842,7 +13842,7 @@ lru-cache@6.0.0, lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" yallist "^4.0.0"
lru-cache@^10.0.1, lru-cache@^10.2.0, lru-cache@^10.2.2: lru-cache@^10.0.1, lru-cache@^10.2.0:
version "10.2.2" version "10.2.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878"
integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==
@ -16803,11 +16803,38 @@ semver-dsl@^1.0.1:
dependencies: dependencies:
semver "^5.3.0" semver "^5.3.0"
"semver@2 || 3 || 4 || 5", semver@7.3.2, semver@7.6.0, semver@7.x, semver@^5.3.0, semver@^5.6.0, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@~7.0.0: "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.6.0:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2:
version "7.3.2" version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0:
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
semver@~7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
send@0.18.0: send@0.18.0:
version "0.18.0" version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@ -17564,9 +17591,6 @@ tar-stream@^2.1.4, tar-stream@~2.2.0:
readable-stream "^3.1.1" readable-stream "^3.1.1"
tar@^6.1.11, tar@^6.1.2, tar@^6.2.0: tar@^6.1.11, tar@^6.1.2, tar@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==

Loading…
Cancel
Save