Browse Source

Merge branch 'main' into feature/improve-language-localization-for-de-20240128

pull/2940/head
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
af272a63e8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 8
      apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts
  3. 9
      apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts
  4. 234
      apps/api/src/app/portfolio/portfolio-calculator.ts
  5. 15
      apps/client/src/app/pages/faq/faq-page.html
  6. 2
      package.json
  7. 8
      yarn.lock

1
CHANGELOG.md

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the tag selector to only show used tags in the assistant (experimental)
- Improved the language localization for German (`de`)
- Upgraded `prettier` from version `3.2.1` to `3.2.4`
### Fixed

8
apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts

@ -1,8 +0,0 @@
import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface';
import Big from 'big.js';
export interface TimelineInfoInterface {
maxNetPerformance: Big;
minNetPerformance: Big;
timelinePeriods: TimelinePeriod[];
}

9
apps/api/src/app/portfolio/interfaces/timeline-period.interface.ts

@ -1,9 +0,0 @@
import Big from 'big.js';
export interface TimelinePeriod {
date: string;
grossPerformance: Big;
investment: Big;
netPerformance: Big;
value: Big;
}

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

@ -1,4 +1,3 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
@ -20,32 +19,19 @@ import {
differenceInDays,
endOfDay,
format,
isAfter,
isBefore,
isSameDay,
isSameMonth,
isSameYear,
max,
min,
set,
subDays
} from 'date-fns';
import {
cloneDeep,
first,
flatten,
isNumber,
last,
sortBy,
uniq
} from 'lodash';
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface';
import {
Accuracy,
TimelineSpecification
@ -776,107 +762,6 @@ export class PortfolioCalculator {
});
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string
): Promise<TimelineInfoInterface> {
if (timelineSpecification.length === 0) {
return {
maxNetPerformance: new Big(0),
minNetPerformance: new Big(0),
timelinePeriods: []
};
}
const startDate = timelineSpecification[0].start;
const start = parseDate(startDate);
const end = parseDate(endDate);
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
let i = 0;
let j = -1;
for (
let currentDate = start;
!isAfter(currentDate, end);
currentDate = this.addToDate(
currentDate,
timelineSpecification[i].accuracy
)
) {
if (this.isNextItemActive(timelineSpecification, currentDate, i)) {
i++;
}
while (
j + 1 < this.transactionPoints.length &&
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate)
) {
j++;
}
let periodEndDate = currentDate;
if (timelineSpecification[i].accuracy === 'day') {
let nextEndDate = end;
if (j + 1 < this.transactionPoints.length) {
nextEndDate = parseDate(this.transactionPoints[j + 1].date);
}
periodEndDate = min([
addMonths(currentDate, 3),
max([currentDate, nextEndDate])
]);
}
const timePeriodForDates = this.getTimePeriodForDate(
j,
currentDate,
endOfDay(periodEndDate)
);
currentDate = periodEndDate;
if (timePeriodForDates != null) {
timelinePeriodPromises.push(timePeriodForDates);
}
}
let minNetPerformance = new Big(0);
let maxNetPerformance = new Big(0);
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises
);
try {
minNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.minNetPerformance)
.filter((performance) => performance !== null)
.reduce((minPerformance, current) => {
if (minPerformance.lt(current)) {
return minPerformance;
} else {
return current;
}
});
maxNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.maxNetPerformance)
.filter((performance) => performance !== null)
.reduce((maxPerformance, current) => {
if (maxPerformance.gt(current)) {
return maxPerformance;
} else {
return current;
}
});
} catch {}
const timelinePeriods = timelineInfoInterfaces.map(
(timelineInfo) => timelineInfo.timelinePeriods
);
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: flatten(timelinePeriods)
};
}
private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0);
let grossPerformance = new Big(0);
@ -983,123 +868,6 @@ export class PortfolioCalculator {
};
}
private async getTimePeriodForDate(
j: number,
startDate: Date,
endDate: Date
): Promise<TimelineInfoInterface> {
let investment: Big = new Big(0);
let fees: Big = new Big(0);
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
if (j >= 0) {
const currencies: { [name: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
for (const item of this.transactionPoints[j].items) {
currencies[item.symbol] = item.currency;
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
investment = investment.plus(item.investment);
fees = fees.plus(item.fee);
}
let marketSymbols: GetValueObject[] = [];
if (dataGatheringItems.length > 0) {
try {
const { values } = await this.currentRateService.getValues({
dataGatheringItems,
dateQuery: {
gte: startDate,
lt: endOfDay(endDate)
}
});
marketSymbols = values;
} catch (error) {
Logger.error(
`Failed to fetch info for date ${startDate} with exception`,
error,
'PortfolioCalculator'
);
return null;
}
}
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
);
}
}
}
const results: TimelinePeriod[] = [];
let maxNetPerformance: Big = null;
let minNetPerformance: Big = null;
for (
let currentDate = startDate;
isBefore(currentDate, endDate);
currentDate = addDays(currentDate, 1)
) {
let value = new Big(0);
const currentDateAsString = format(currentDate, DATE_FORMAT);
let invalid = false;
if (j >= 0) {
for (const item of this.transactionPoints[j].items) {
if (
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol)
) {
invalid = true;
break;
}
value = value.plus(
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol])
);
}
}
if (!invalid) {
const grossPerformance = value.minus(investment);
const netPerformance = grossPerformance.minus(fees);
if (
minNetPerformance === null ||
minNetPerformance.gt(netPerformance)
) {
minNetPerformance = netPerformance;
}
if (
maxNetPerformance === null ||
maxNetPerformance.lt(netPerformance)
) {
maxNetPerformance = netPerformance;
}
const result = {
grossPerformance,
investment,
netPerformance,
value,
date: currentDateAsString
};
results.push(result);
}
}
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: results
};
}
private getFactor(type: TypeOfOrder) {
let factor: number;

15
apps/client/src/app/pages/faq/faq-page.html

@ -50,11 +50,8 @@
You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a
>” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token, using
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>Internet Identity</a
>
or <i>Google Sign</i>. We will guide you to set up your portfolio.
Ghostfolio: Create an account with a security token or
<i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
@ -75,11 +72,9 @@
></mat-card-header
>
<mat-card-content>
Yes, the authentication systems (via security token or
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity"
>Internet Identity</a
>) enable you to sign in securely and anonymously to Ghostfolio. There
is no need for an e-mail address, phone number, or a username.
Yes, the authentication system via security token enables you to sign
in securely and anonymously to Ghostfolio. There is no need for an
e-mail address, phone number, or a username.
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">

2
package.json

@ -191,7 +191,7 @@
"jest-environment-jsdom": "29.4.3",
"jest-preset-angular": "13.1.4",
"nx": "17.2.8",
"prettier": "3.2.1",
"prettier": "3.2.4",
"prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",

8
yarn.lock

@ -16945,10 +16945,10 @@ prettier-plugin-organize-attributes@1.0.0:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
prettier@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.1.tgz#babf33580e16c796a9740b9fae551624f7bfeaab"
integrity sha512-qSUWshj1IobVbKc226Gw2pync27t0Kf0EdufZa9j7uBSJay1CC+B3K5lAAZoqgX3ASiKuWsk6OmzKRetXNObWg==
prettier@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
prettier@^2.8.0:
version "2.8.8"

Loading…
Cancel
Save