Compare commits

..

13 Commits

Author SHA1 Message Date
86a1589834 Release/1.128.0 (#763) 2022-03-19 14:39:25 +01:00
9f67993c03 Feature/fix issue with recent transactions (#750)
* Fix percentage performance issue with recent transactions

Co-authored-by: Reto Kaul <retokaul@sublimd.com>
2022-03-19 14:33:43 +01:00
32fb3551dc Feature/add default market price to scraper configuration (#762)
* Add default market price to scraper configuration

* Update changelog
2022-03-19 12:17:28 +01:00
30411b1502 Feature/add hover to table (#760)
* Add hover

* Update changelog
2022-03-19 09:56:50 +01:00
eb0444603b Bugfix/fix user currency of public page (#761)
* Fix user currency

* Update changelog
2022-03-19 09:25:20 +01:00
6e582fe505 Release 1.127.0 (#759) 2022-03-16 22:08:48 +01:00
402d73a12c Bugfix/fix get quotes for multiple ghostfolio symbols (#758)
* Support multiple symbols in getQuotes()

* Update changelog
2022-03-16 22:07:18 +01:00
4826a51199 Feature/improve handling of scraper configuration (#757)
* Improve handling of missing scraper configuration

* Update changelog
2022-03-15 17:13:42 +01:00
5356bf568e Release 1.126.0 (#756) 2022-03-14 17:41:23 +01:00
d8da574ae4 Feature/add support for bonds (#755)
* Add support for bonds

* Update changelog
2022-03-14 17:39:09 +01:00
e769fabbae Feature/add multilines to tooltips in proportion chart (#753)
* Introduce multilines for tooltips

* Update changelog
2022-03-13 21:39:06 +01:00
5a369f29d4 Feature/restructure portfolio summary tab (#754)
* Restructure portfolio summary

* Update changelog
2022-03-13 21:01:15 +01:00
122ba9046f Add type (#751)
* Add type

* Refactor import to import type
2022-03-13 21:00:40 +01:00
15 changed files with 163 additions and 54 deletions

View File

@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.128.0 - 19.03.2022
### Added
- Added the attribute `defaultMarketPrice` to the scraper configuration to improve the support for bonds
- Added a hover effect to the table style
### Fixed
- Fixed an issue with the user currency of the public page
- Fixed an issue of the performance calculation with recent activities in the new calculation engine
## 1.127.0 - 16.03.2022
### Changed
- Improved the error handling in the scraper configuration
### Fixed
- Fixed the support for multiple symbols of the data source `GHOSTFOLIO`
## 1.126.0 - 14.03.2022
### Added
- Added support for bonds
### Changed
- Restructured the portfolio summary tab on the home page
- Improved the tooltips in the portfolio proportion chart component by introducing multilines
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.125.0 - 12.03.2022
### Added

View File

@ -37,6 +37,9 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculatorNew {
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT =
true;
private static readonly ENABLE_LOGGING = false;
private currency: string;
@ -688,6 +691,7 @@ export class PortfolioCalculatorNew {
let grossPerformanceAtStartDate = new Big(0);
let grossPerformanceFromSells = new Big(0);
let initialValue: Big;
let investmentAtStartDate: Big;
let lastAveragePrice = new Big(0);
let lastTransactionInvestment = new Big(0);
let lastValueOfInvestmentBeforeTransaction = new Big(0);
@ -697,6 +701,7 @@ export class PortfolioCalculatorNew {
let totalInvestment = new Big(0);
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
let totalUnits = new Big(0);
let valueAtStartDate: Big;
// Add a synthetic order at the start and the end date
orders.push({
@ -774,13 +779,18 @@ export class PortfolioCalculatorNew {
order.unitPrice
);
if (!investmentAtStartDate && i >= indexOfStartOrder) {
investmentAtStartDate = totalInvestment ?? new Big(0);
valueAtStartDate = valueOfInvestmentBeforeTransaction;
}
const transactionInvestment = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
totalInvestment = totalInvestment.plus(transactionInvestment);
if (totalInvestment.gt(maxTotalInvestment)) {
if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) {
maxTotalInvestment = totalInvestment;
}
@ -898,12 +908,22 @@ export class PortfolioCalculatorNew {
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus(
maxTotalInvestment.minus(investmentAtStartDate)
);
const grossPerformancePercentage =
PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) ||
orders[indexOfStartOrder].unitPrice.eq(0)
? totalGrossPerformance.div(maxTotalInvestment)
: unitPriceAtEndDate
? maxInvestmentBetweenStartAndEndDate.gt(0)
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
: new Big(0)
: // This formula has the issue that buying more units with a price
// lower than the average buying price results in a positive
// performance even if the market price stays constant
unitPriceAtEndDate
.div(averagePriceAtEndDate)
.div(
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
@ -915,11 +935,17 @@ export class PortfolioCalculatorNew {
: new Big(0);
const netPerformancePercentage =
PortfolioCalculatorNew.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) ||
orders[indexOfStartOrder].unitPrice.eq(0)
? totalNetPerformance.div(maxTotalInvestment)
: unitPriceAtEndDate
? maxInvestmentBetweenStartAndEndDate.gt(0)
? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate)
: new Big(0)
: // This formula has the issue that buying more units with a price
// lower than the average buying price results in a positive
// performance even if the market price stays constant
unitPriceAtEndDate
.minus(feesPerUnit)
.div(averagePriceAtEndDate)
.div(

View File

@ -307,7 +307,10 @@ export class PortfolioServiceNew {
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const userCurrency =
this.request.user?.Settings?.currency ??
user.Settings?.currency ??
baseCurrency;
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({

View File

@ -298,7 +298,10 @@ export class PortfolioService {
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const userCurrency =
this.request.user?.Settings?.currency ??
user.Settings?.currency ??
baseCurrency;
const portfolioCalculator = new PortfolioCalculator(
this.currentRateService,
userCurrency

View File

@ -13,7 +13,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { format } from 'date-fns';
import { addDays, format, isBefore } from 'date-fns';
@Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface {
@ -50,16 +50,36 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[symbol]
);
const scraperConfiguration = symbolProfile?.scraperConfiguration;
const { defaultMarketPrice, selector, url } =
symbolProfile.scraperConfiguration;
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
if (defaultMarketPrice) {
const historical: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
} = {
[symbol]: {}
};
let date = from;
while (isBefore(date, to)) {
historical[symbol][format(date, DATE_FORMAT)] = {
marketPrice: defaultMarketPrice
};
date = addDays(date, 1);
}
return historical;
} else if (selector === undefined || url === undefined) {
return {};
}
const get = bent(url, 'GET', 'string', 200, {});
const html = await get();
const $ = cheerio.load(html);
const value = this.extractNumberFromString(
$(scraperConfiguration?.selector).text()
);
const value = this.extractNumberFromString($(selector).text());
return {
[symbol]: {
@ -82,33 +102,42 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
public async getQuotes(
aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {};
if (aSymbols.length <= 0) {
return {};
return response;
}
try {
const [symbol] = aSymbols;
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[symbol]
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
aSymbols
);
const { marketPrice } = await this.prismaService.marketData.findFirst({
const marketData = await this.prismaService.marketData.findMany({
distinct: ['symbol'],
orderBy: {
date: 'desc'
},
take: aSymbols.length,
where: {
symbol
symbol: {
in: aSymbols
}
}
});
return {
[symbol]: {
marketPrice,
currency: symbolProfile?.currency,
for (const symbolProfile of symbolProfiles) {
response[symbolProfile.symbol] = {
currency: symbolProfile.currency,
dataSource: this.getName(),
marketPrice: marketData.find((marketDataItem) => {
return marketDataItem.symbol === symbolProfile.symbol;
}).marketPrice,
marketState: MarketState.delayed
}
};
};
}
return response;
} catch (error) {
Logger.error(error, 'GhostfolioScraperApiService');
}

View File

@ -1,4 +1,5 @@
export interface ScraperConfiguration {
defaultMarketPrice?: number;
selector: string;
url: string;
}

View File

@ -21,6 +21,7 @@ import Big from 'big.js';
import { countries } from 'countries-list';
import { addDays, format, isSameDay } from 'date-fns';
import yahooFinance from 'yahoo-finance2';
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
@ -303,7 +304,7 @@ export class YahooFinanceService implements DataProviderInterface {
return { items };
}
private parseAssetClass(aPrice: any): {
private parseAssetClass(aPrice: Price): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {

View File

@ -79,6 +79,7 @@ export class SymbolProfileService {
if (scraperConfiguration) {
return {
defaultMarketPrice: scraperConfiguration.defaultMarketPrice as number,
selector: scraperConfiguration.selector as string,
url: scraperConfiguration.url as string
};

View File

@ -119,7 +119,7 @@
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Value</div>
<div class="d-flex flex-grow-1" i18n>Total</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
@ -130,6 +130,17 @@
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Valuables</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.items"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Emergency Fund</div>
<div
@ -151,7 +162,7 @@
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
<div class="d-flex flex-grow-1" i18n>Buying Power</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
@ -161,17 +172,6 @@
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Items</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.items"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>

View File

@ -11,20 +11,22 @@
.mat-row {
&:nth-child(even) {
background-color: rgba(
var(--palette-foreground-base),
var(--palette-background-hover-alpha)
);
background-color: rgba(var(--palette-foreground-base), 0.02);
}
&:hover {
background-color: rgba(var(--palette-foreground-base), 0.05);
}
}
@if $darkTheme {
.mat-row {
&:nth-child(even) {
background-color: rgba(
var(--palette-foreground-base-dark),
var(--palette-background-hover-alpha)
);
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
}
&:hover {
background-color: rgba(var(--palette-foreground-base-dark), 0.05);
}
}
}

View File

@ -324,16 +324,16 @@ export class PortfolioProportionChartComponent
const percentage = (context.parsed * 100) / sum;
if (this.isInPercent) {
return `${name ?? symbol} (${percentage.toFixed(2)}%)`;
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
} else {
const value = <number>context.raw;
return `${name ?? symbol}: ${value.toLocaleString(
this.locale,
{
return [
`${name ?? symbol}`,
`${value.toLocaleString(this.locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
}
)} ${this.baseCurrency} (${percentage.toFixed(2)}%)`;
})} ${this.baseCurrency} (${percentage.toFixed(2)}%)`
];
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.125.0",
"version": "1.128.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "AssetClass" ADD VALUE 'FIXED_INCOME';

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "AssetSubClass" ADD VALUE 'BOND';

View File

@ -171,9 +171,11 @@ enum AssetClass {
CASH
COMMODITY
EQUITY
FIXED_INCOME
}
enum AssetSubClass {
BOND
CRYPTOCURRENCY
ETF
MUTUALFUND