Compare commits

..

9 Commits

Author SHA1 Message Date
6a0cfb8f77 Release 1.44.0 (#323) 2021-08-30 18:22:41 +02:00
6386786ac0 Bugfix/improve symbol lookup (#322)
* Improve symbol lookup

* Update changelog
2021-08-30 18:08:21 +02:00
d3be6577c8 Add feature: time-weighted rate of return (TWR) (#321) 2021-08-29 17:09:11 +02:00
73a967a7e5 Feature/add cash as asset sub class (#319)
* Add cash as asset sub class

* Update changelog
2021-08-26 21:49:02 +02:00
836ff6ec13 Feature/upgrade svgmap to version 2.6.0 (#318)
* Upgrade svgmap

* Update changelog
2021-08-26 21:23:49 +02:00
c5bb3023d3 Bugfix/filter out positions without quantity (#317)
* Filter out positions without any quantity

* Update changelog
2021-08-26 17:45:04 +02:00
695c378b48 Release 1.43.0 (#316) 2021-08-24 21:31:19 +02:00
fe975945d1 Feature/add fallback for loading currencies (#315)
* Add fallback for loading currencies

* Update changelog
2021-08-24 21:09:02 +02:00
d8782b0d4c Feature/automate countries for stocks in symbol profile data (#314)
* Automate countries for stocks in symbol profile data

* Update changelog
2021-08-24 20:24:18 +02:00
15 changed files with 130 additions and 35 deletions

View File

@ -5,6 +5,25 @@ 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.44.0 - 30.08.2021
### Changed
- Extended the sub classification of assets by cash
- Upgraded `svgmap` from version `2.1.1` to `2.6.0`
### Fixed
- Filtered out positions without any quantity in the positions table
- Improved the symbol lookup: allow saving with valid symbol in create or edit transaction dialog
## 1.43.0 - 24.08.2021
### Added
- Extended the data management of symbol profile data by countries (automated for stocks)
- Added a fallback for initially loading currencies if historical data is not yet available
## 1.42.0 - 22.08.2021
### Added

View File

@ -62,7 +62,7 @@ Ghostfolio is for you if you are...
- ✅ Create, update and delete transactions
- ✅ Multi account management
- ✅ Portfolio performance (`Today`, `YTD`, `1Y`, `5Y`, `Max`)
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `YTD`, `1Y`, `5Y`, `Max`
- ✅ Various charts
- ✅ Static analysis to identify potential risks in your portfolio
- ✅ Dark Mode

View File

@ -211,6 +211,11 @@ export class PortfolioService {
}
for (const item of currentPositions.positions) {
if (item.quantity.lte(0)) {
// Ignore positions without any quantity
continue;
}
const value = item.quantity.mul(item.marketPrice);
const symbolProfile = symbolProfileMap[item.symbol];
const dataProviderResponse = dataProviderResponses[item.symbol];
@ -719,6 +724,7 @@ export class PortfolioService {
allocationCurrent: cashValue.div(value).toNumber(),
allocationInvestment: cashValue.div(investment).toNumber(),
assetClass: AssetClass.CASH,
assetSubClass: AssetClass.CASH,
countries: [],
currency: Currency.CHF,
grossPerformance: 0,

View File

@ -11,6 +11,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -48,6 +49,15 @@ export class SymbolController {
@Get(':symbol')
@UseGuards(AuthGuard('jwt'))
public async getPosition(@Param('symbol') symbol): Promise<SymbolItem> {
return this.symbolService.get(symbol);
const result = await this.symbolService.get(symbol);
if (!result || isEmpty(result)) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return result;
}
}

View File

@ -15,13 +15,17 @@ export class SymbolService {
public async get(aSymbol: string): Promise<SymbolItem> {
const response = await this.dataProviderService.get([aSymbol]);
const { currency, dataSource, marketPrice } = response[aSymbol];
const { currency, dataSource, marketPrice } = response[aSymbol] ?? {};
return {
dataSource,
marketPrice,
currency: <Currency>(<unknown>currency)
};
if (currency && dataSource && marketPrice) {
return {
dataSource,
marketPrice,
currency: <Currency>(<unknown>currency)
};
}
return undefined;
}
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {

View File

@ -136,13 +136,14 @@ export class DataGatheringService {
for (const [
symbol,
{ assetClass, assetSubClass, currency, dataSource, name }
{ assetClass, assetSubClass, countries, currency, dataSource, name }
] of Object.entries(currentData)) {
try {
await this.prismaService.symbolProfile.upsert({
create: {
assetClass,
assetSubClass,
countries,
currency,
dataSource,
name,
@ -151,6 +152,7 @@ export class DataGatheringService {
update: {
assetClass,
assetSubClass,
countries,
currency,
name
},

View File

@ -25,6 +25,7 @@ export interface IYahooFinancePrice {
}
export interface IYahooFinanceSummaryProfile {
country?: string;
industry?: string;
sector?: string;
website?: string;

View File

@ -16,6 +16,7 @@ import {
} from '@prisma/client';
import * as bent from 'bent';
import Big from 'big.js';
import { countries } from 'countries-list';
import { format } from 'date-fns';
import * as yahooFinance from 'yahoo-finance';
@ -92,6 +93,23 @@ export class YahooFinanceService implements DataProviderInterface {
.toNumber();
}
// Add country if stock and available
if (
assetSubClass === AssetSubClass.STOCK &&
value.summaryProfile?.country
) {
try {
const [code] = Object.entries(countries).find(([, country]) => {
return country.name === value.summaryProfile?.country;
});
if (code) {
response[symbol].countries = [{ code, weight: 1 }];
}
} catch {}
}
// Add url if available
const url = value.summaryProfile?.website;
if (url) {
response[symbol].url = url;

View File

@ -3,7 +3,7 @@ import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client';
import { format } from 'date-fns';
import { isNumber } from 'lodash';
import { isEmpty, isNumber } from 'lodash';
import { DataProviderService } from './data-provider/data-provider.service';
@ -35,6 +35,24 @@ export class ExchangeRateDataService {
getYesterday()
);
if (isEmpty(result)) {
// Load currencies directly from data provider as a fallback
// if historical data is not yet available
const historicalData = await this.dataProviderService.get(
this.currencyPairs.map((currencyPair) => {
return currencyPair;
})
);
Object.keys(historicalData).forEach((key) => {
result[key] = {
[format(getYesterday(), DATE_FORMAT)]: {
marketPrice: historicalData[key].marketPrice
}
};
});
}
const resultExtended = result;
Object.keys(result).forEach((pair) => {

View File

@ -37,6 +37,7 @@ export interface IDataProviderHistoricalResponse {
export interface IDataProviderResponse {
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
countries?: { code: string; weight: number }[];
currency: Currency;
dataSource: DataSource;
exchange?: string;

View File

@ -11,8 +11,9 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Currency } from '@prisma/client';
import { Observable, Subject } from 'rxjs';
import { EMPTY, Observable, Subject } from 'rxjs';
import {
catchError,
debounceTime,
distinctUntilChanged,
startWith,
@ -49,7 +50,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
) {}
ngOnInit() {
public ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies;
@ -84,17 +85,45 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.data.transaction.unitPrice = this.currentMarketPrice;
}
public onBlurSymbol() {
const symbol = this.searchSymbolCtrl.value;
this.updateSymbol(symbol);
}
public onCancel(): void {
this.dialogRef.close();
}
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.updateSymbol(event.option.value);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private updateSymbol(symbol: string) {
this.isLoading = true;
this.data.transaction.symbol = event.option.value;
this.data.transaction.symbol = symbol;
this.dataService
.fetchSymbolItem(this.data.transaction.symbol)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(
catchError(() => {
this.data.transaction.currency = null;
this.data.transaction.dataSource = null;
this.data.transaction.unitPrice = null;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ currency, dataSource, marketPrice }) => {
this.data.transaction.currency = currency;
this.data.transaction.dataSource = dataSource;
@ -105,17 +134,4 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
this.changeDetectorRef.markForCheck();
});
}
public onUpdateSymbolByTyping(value: string) {
this.data.transaction.currency = null;
this.data.transaction.dataSource = null;
this.data.transaction.unitPrice = null;
this.data.transaction.symbol = value;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -29,7 +29,7 @@
required
[formControl]="searchSymbolCtrl"
[matAutocomplete]="auto"
(change)="onUpdateSymbolByTyping($event.target.value)"
(blur)="onBlurSymbol()"
/>
<mat-autocomplete
#auto="matAutocomplete"

View File

@ -8,7 +8,7 @@ export interface PortfolioPosition {
allocationCurrent: number;
allocationInvestment: number;
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
assetSubClass?: AssetSubClass | 'CASH';
countries: Country[];
currency: Currency;
exchange?: string;

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.42.0",
"version": "1.44.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -103,7 +103,7 @@
"round-to": "5.0.0",
"rxjs": "6.6.7",
"stripe": "8.156.0",
"svgmap": "2.1.1",
"svgmap": "2.6.0",
"uuid": "8.3.2",
"yahoo-finance": "0.3.6",
"zone.js": "0.11.4"

View File

@ -12991,10 +12991,10 @@ svg-pan-zoom@^3.6.1:
resolved "https://registry.yarnpkg.com/svg-pan-zoom/-/svg-pan-zoom-3.6.1.tgz#f880a1bb32d18e9c625d7715350bebc269b450cf"
integrity sha512-JaKkGHHfGvRrcMPdJWkssLBeWqM+Isg/a09H7kgNNajT1cX5AztDTNs+C8UzpCxjCTRrG34WbquwaovZbmSk9g==
svgmap@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/svgmap/-/svgmap-2.1.1.tgz#355c259cf4e04b20d2d39bab05d0e718ade942ff"
integrity sha512-1blZYMYDXq8H3xykzgBJRh5q+XPd5JLOJ8K7UuZI6ab2D3hngiVcr+Z1olfy7DH9Xf9AOCTpt4Id7iVD8cKD0A==
svgmap@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/svgmap/-/svgmap-2.6.0.tgz#8533f40d3c1015d25f5e799e677b794acdd5fbc5"
integrity sha512-MePkVjgYlHwEfCSuGt+wB6WX6Z2fTD6yDtqZO5syzKYH7gamt1Hp9f/Bw5R49OvBtbsF7WCaGR0/GhewZGGA+w==
dependencies:
svg-pan-zoom "^3.6.1"