Compare commits

..

20 Commits

Author SHA1 Message Date
0de28d733e Release 1.187.0 (#1227) 2022-09-03 21:42:23 +02:00
3b2f13850c Feature/improve chart calculation (#1226)
* Improve chart calculation

* Update changelog
2022-09-03 21:41:06 +02:00
0cc42ffd7c Add end date parameter (#1224)
* Add end date parameter
2022-09-03 21:28:57 +02:00
3ccb812ac3 Release 1.186.2 (#1223) 2022-09-03 11:37:14 +02:00
0a8549db3e Release 1.186.1 (#1222) 2022-09-03 11:19:31 +02:00
c95e90ff31 Release 1.186.0 (#1221) 2022-09-03 10:28:20 +02:00
b59af0d864 Feature/upgrade nx to version 14.6.4 (#1220)
* Upgrade nx and angular

* Update changelog
2022-09-03 10:26:56 +02:00
408bdbd187 Bugfix/fix GitHub contributors count (#1219)
* Fix GitHub contributors count

* Update changelog
2022-09-03 10:06:16 +02:00
a3bfa46fb0 Feature/remove alias from user (#1218)
* Remove alias

* Update changelog
2022-09-03 09:47:18 +02:00
8cb1b3f925 Feature/decrease rate limiter duration (#1217)
* Decrease rate limiter duration

* Update changelog
2022-09-03 09:09:57 +02:00
15c650f951 Bugfix/fix blog post link (#1216)
* Fix link

* Update sitemap.xml
2022-09-03 09:09:37 +02:00
c198bd78da Add architectures (#1205) 2022-09-03 08:32:01 +02:00
35963580bc Bugfix/improve error handling in portfolio calculations (#1215)
* Improve error handling

* Update changelog
2022-09-03 08:31:47 +02:00
cf2c5bad02 Bugfix/change environment variables redis host and port to mandatory (#1211)
* Change REDIS_HOST and REDIS_PORT to mandatory

* Update changelog
2022-09-01 15:35:37 +02:00
f332aea9b4 Release 1.185.0 (#1207) 2022-08-30 20:53:26 +02:00
7a9fd18407 Feature/improve markets overview (#1206)
* Improve markets overview

* Update changelog
2022-08-30 20:52:13 +02:00
ca08d3154a Bugfix/disable language selector for demo user (#1204)
* Disable language selector for demo user

* Update changelog
2022-08-28 21:11:27 +02:00
01d4ae8757 Feature/move build pipeline from travis to GitHub actions (#1203)
* Remove travis configurations

* Update changelog
2022-08-28 21:10:25 +02:00
43ce2786c1 Fetch all history for all tags and branches (#1202) 2022-08-28 11:01:54 +02:00
de2092c4d2 Release 1.184.2 (#1201) 2022-08-28 10:09:59 +02:00
49 changed files with 2204 additions and 1637 deletions

View File

@ -1,6 +1,7 @@
name: Build code
on:
pull_request:
workflow_dispatch:
jobs:
@ -13,6 +14,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v3

View File

@ -21,7 +21,7 @@ jobs:
with:
images: ghostfolio/ghostfolio
tags: |
type=raw,value=linux-arm64-{{version}}
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
@ -41,7 +41,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/arm64
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.output.labels }}

View File

@ -1,30 +0,0 @@
language: node_js
git:
depth: false
node_js:
- 16
services:
- docker
cache: yarn
if: (type = pull_request) OR (tag IS present)
jobs:
include:
- stage: Install dependencies
if: type = pull_request
script: yarn --frozen-lockfile
- stage: Check formatting
if: type = pull_request
script: yarn format:check
- stage: Execute tests
if: type = pull_request
script: yarn test
- stage: Build application
if: type = pull_request
script: yarn build:all
- stage: Build and publish docker image
if: tag IS present
script: ./publish-docker-image.sh

View File

@ -5,7 +5,44 @@ 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.184.1 - 28.08.2022
## 1.187.0 - 03.09.2022
### Added
- Supported units in the line chart component
- Added a new chart calculation engine (experimental)
## 1.186.2 - 03.09.2022
### Changed
- Decreased the rate limiter duration of queue jobs from 5 to 4 seconds
- Removed the alias from the `User` database schema
- Upgraded `angular` from version `14.1.0` to `14.2.0`
- Upgraded `Nx` from version `14.5.1` to `14.6.4`
### Fixed
- Fixed the environment variables `REDIS_HOST`, `REDIS_PASSWORD` and `REDIS_PORT` in the Redis configuration
- Handled errors in the portfolio calculation if there is no internet connection
- Fixed the _GitHub_ contributors count on the about page
## 1.185.0 - 30.08.2022
### Added
- Added a skeleton loader to the market mood component in the markets overview
### Changed
- Moved the build pipeline from _Travis_ to _GitHub Actions_
- Increased the caching of the benchmarks
### Fixed
- Disabled the language selector for the demo user
## 1.184.2 - 28.08.2022
### Added

View File

@ -17,8 +17,6 @@
<p>
<a href="#contributing">
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a>
</p>
@ -81,6 +79,8 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
## Self-hosting
We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64` and `linux/arm64`.
### Supported Environment Variables
| Name | Default Value | Description |
@ -94,9 +94,9 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
| `REDIS_HOST` | `localhost` | The host where _Redis_ is running |
| `REDIS_HOST` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | | The password of _Redis_ |
| `REDIS_PORT` | `6379` | The port where _Redis_ is running |
| `REDIS_PORT` | | The port where _Redis_ is running |
### Run with Docker Compose

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default {
displayName: 'api',

View File

@ -54,7 +54,7 @@ export class WebAuthService {
rpName: 'Ghostfolio',
rpID: this.rpID,
userID: user.id,
userName: user.alias,
userName: '',
timeout: 60000,
attestationType: 'indirect',
authenticatorSelection: {

View File

@ -1,30 +1,20 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { BenchmarkResponse } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { BenchmarkService } from './benchmark.service';
@Controller('benchmark')
export class BenchmarkController {
public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly propertyService: PropertyService
) {}
public constructor(private readonly benchmarkService: BenchmarkService) {}
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {
const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
return {
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets)
benchmarks: await this.benchmarkService.getBenchmarks()
};
}
}

View File

@ -1,10 +1,13 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import Big from 'big.js';
import ms from 'ms';
@Injectable()
export class BenchmarkService {
@ -13,15 +16,17 @@ export class BenchmarkService {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService,
private readonly symbolProfileService: SymbolProfileService
) {}
public async getBenchmarks(
benchmarkAssets: UniqueAsset[]
): Promise<BenchmarkResponse['benchmarks']> {
public async getBenchmarks({ useCache = true } = {}): Promise<
BenchmarkResponse['benchmarks']
> {
let benchmarks: BenchmarkResponse['benchmarks'];
if (useCache) {
try {
benchmarks = JSON.parse(
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
@ -31,7 +36,12 @@ export class BenchmarkService {
return benchmarks;
}
} catch {}
}
const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
const promises: Promise<number>[] = [];
const [quotes, assetProfiles] = await Promise.all([
@ -76,7 +86,8 @@ export class BenchmarkService {
await this.redisCacheService.set(
this.CACHE_KEY_BENCHMARKS,
JSON.stringify(benchmarks)
JSON.stringify(benchmarks),
ms('4 hours') / 1000
);
return benchmarks;

View File

@ -1,6 +1,5 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
@ -13,7 +12,10 @@ import {
PROPERTY_SYSTEM_MESSAGE,
ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config';
import { encodeDataSource } from '@ghostfolio/common/helper';
import {
encodeDataSource,
extractNumberFromString
} from '@ghostfolio/common/helper';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
@ -21,6 +23,7 @@ import { permissions } from '@ghostfolio/common/permissions';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { subDays } from 'date-fns';
@Injectable()
@ -30,7 +33,6 @@ export class InfoService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly dataGatheringService: DataGatheringService,
private readonly jwtService: JwtService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
@ -143,17 +145,21 @@ export class InfoService {
private async countGitHubContributors(): Promise<number> {
try {
const get = bent(
`https://api.github.com/repos/ghostfolio/ghostfolio/contributors`,
'https://github.com/ghostfolio/ghostfolio',
'GET',
'json',
'string',
200,
{
'User-Agent': 'request'
}
{}
);
const contributors = await get();
return contributors?.length;
const html = await get();
const $ = cheerio.load(html);
return extractNumberFromString(
$(
`a[href="/ghostfolio/ghostfolio/graphs/contributors"] .Counter`
).text()
);
} catch (error) {
Logger.error(error, 'InfoService');

View File

@ -16,6 +16,7 @@ import {
isBefore,
isSameMonth,
isSameYear,
isWithinInterval,
max,
min,
set
@ -167,13 +168,21 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints;
}
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) {
public async getCurrentPositions(
start: Date,
end = new Date(Date.now())
): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
if (!transactionPointsBeforeEndDate.length) {
return {
currentValue: new Big(0),
hasErrors: false,
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
hasErrors: false,
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
positions: [],
@ -182,39 +191,38 @@ export class PortfolioCalculator {
}
const lastTransactionPoint =
this.transactionPoints[this.transactionPoints.length - 1];
// use Date.now() to use the mock for today
const today = new Date(Date.now());
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
let firstTransactionPoint: TransactionPoint = null;
let firstIndex = this.transactionPoints.length;
let firstIndex = transactionPointsBeforeEndDate.length;
const dates = [];
const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {};
dates.push(resetHours(start));
for (const item of this.transactionPoints[firstIndex - 1].items) {
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency;
}
for (let i = 0; i < this.transactionPoints.length; i++) {
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
if (
!isBefore(parseDate(this.transactionPoints[i].date), start) &&
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
firstTransactionPoint === null
) {
firstTransactionPoint = this.transactionPoints[i];
firstTransactionPoint = transactionPointsBeforeEndDate[i];
firstIndex = i;
}
if (firstTransactionPoint !== null) {
dates.push(resetHours(parseDate(this.transactionPoints[i].date)));
dates.push(
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
}
}
dates.push(resetHours(today));
dates.push(resetHours(end));
const marketSymbols = await this.currentRateService.getValues({
currencies,
@ -241,7 +249,7 @@ export class PortfolioCalculator {
}
}
const todayString = format(today, DATE_FORMAT);
const endDateString = format(end, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
@ -254,7 +262,7 @@ export class PortfolioCalculator {
const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
const marketValue = marketSymbolMap[endDateString]?.[item.symbol];
const {
grossPerformance,
@ -264,6 +272,7 @@ export class PortfolioCalculator {
netPerformance,
netPerformancePercentage
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
symbol: item.symbol
@ -432,10 +441,15 @@ export class PortfolioCalculator {
}
}
let minNetPerformance = new Big(0);
let maxNetPerformance = new Big(0);
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises
);
const minNetPerformance = timelineInfoInterfaces
try {
minNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.minNetPerformance)
.filter((performance) => performance !== null)
.reduce((minPerformance, current) => {
@ -446,7 +460,7 @@ export class PortfolioCalculator {
}
});
const maxNetPerformance = timelineInfoInterfaces
maxNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.maxNetPerformance)
.filter((performance) => performance !== null)
.reduce((maxPerformance, current) => {
@ -456,6 +470,7 @@ export class PortfolioCalculator {
return current;
}
});
} catch {}
const timelinePeriods = timelineInfoInterfaces.map(
(timelineInfo) => timelineInfo.timelinePeriods
@ -694,10 +709,12 @@ export class PortfolioCalculator {
}
private getSymbolMetrics({
end,
marketSymbolMap,
start,
symbol
}: {
end: Date;
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
@ -720,13 +737,12 @@ export class PortfolioCalculator {
}
const dateOfFirstTransaction = new Date(first(orders).date);
const endDate = new Date(Date.now());
const unitPriceAtStartDate =
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
const unitPriceAtEndDate =
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol];
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
if (
!unitPriceAtEndDate ||
@ -779,7 +795,7 @@ export class PortfolioCalculator {
orders.push({
symbol,
currency: null,
date: format(endDate, DATE_FORMAT),
date: format(end, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
itemType: 'end',

View File

@ -35,7 +35,8 @@ import {
Param,
Query,
UseGuards,
UseInterceptors
UseInterceptors,
Version
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@ -110,6 +111,26 @@ export class PortfolioController {
};
}
@Get('chart')
@UseGuards(AuthGuard('jwt'))
@Version('2')
public async getChartV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChartV2(
impersonationId,
range
);
return {
chart: historicalDataContainer.items,
hasError: false,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
@Get('details')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)

View File

@ -57,6 +57,7 @@ import {
} from '@prisma/client';
import Big from 'big.js';
import {
addDays,
differenceInDays,
endOfToday,
format,
@ -71,7 +72,7 @@ import {
subDays,
subYears
} from 'date-fns';
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash';
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
import {
HistoricalDataContainer,
@ -85,6 +86,7 @@ const emergingMarkets = require('../../assets/countries/emerging-markets.json');
@Injectable()
export class PortfolioService {
private static readonly MAX_CHART_ITEMS = 250;
private baseCurrency: string;
public constructor(
@ -327,10 +329,10 @@ export class PortfolioService {
}
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
lastItem?.netPerformance
lastItem?.netPerformance ?? 0
);
let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
lastItem?.netPerformance
lastItem?.netPerformance ?? 0
);
if (isAllTimeHigh && isAllTimeLow) {
isAllTimeHigh = false;
@ -354,6 +356,78 @@ export class PortfolioService {
};
}
public async getChartV2(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, PortfolioService.MAX_CHART_ITEMS)
);
const items: HistoricalDataItem[] = [];
let currentEndDate = startDate;
while (isBefore(currentEndDate, endDate)) {
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate,
currentEndDate
);
items.push({
date: format(currentEndDate, DATE_FORMAT),
value: currentPositions.netPerformancePercentage.toNumber() * 100
});
currentEndDate = addDays(currentEndDate, step);
}
const today = new Date();
if (last(items)?.date !== format(today, DATE_FORMAT)) {
// Add today
const { netPerformancePercentage } =
await portfolioCalculator.getCurrentPositions(startDate, today);
items.push({
date: format(today, DATE_FORMAT),
value: netPerformancePercentage.toNumber() * 100
});
}
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: items
};
}
public async getDetails(
aImpersonationId: string,
aUserId: string,
@ -466,7 +540,9 @@ export class PortfolioService {
holdings[item.symbol] = {
markets,
allocationCurrent: value.div(totalValue).toNumber(),
allocationCurrent: totalValue.eq(0)
? 0
: value.div(totalValue).toNumber(),
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass,
@ -478,7 +554,7 @@ export class PortfolioService {
item.grossPerformancePercentage?.toNumber() ?? 0,
investment: item.investment.toNumber(),
marketPrice: item.marketPrice,
marketState: dataProviderResponse.marketState,
marketState: dataProviderResponse?.marketState ?? 'delayed',
name: symbolProfile.name,
netPerformance: item.netPerformance?.toNumber() ?? 0,
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,

View File

@ -1,7 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { CacheModule, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CacheManagerOptions, CacheModule, Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
import { RedisCacheService } from './redis-cache.service';
@ -9,16 +8,18 @@ import { RedisCacheService } from './redis-cache.service';
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configurationService: ConfigurationService) => ({
imports: [ConfigurationModule],
inject: [ConfigurationService],
useFactory: async (configurationService: ConfigurationService) => {
return <CacheManagerOptions>{
host: configurationService.get('REDIS_HOST'),
max: configurationService.get('MAX_ITEM_IN_CACHE'),
password: configurationService.get('REDIS_PASSWORD'),
port: configurationService.get('REDIS_PORT'),
store: redisStore,
ttl: configurationService.get('CACHE_TTL')
})
};
}
}),
ConfigurationModule
],

View File

@ -5,6 +5,10 @@ export class UpdateUserSettingDto {
@IsOptional()
emergencyFund?: number;
@IsBoolean()
@IsOptional()
isExperimentalFeatures?: boolean;
@IsBoolean()
@IsOptional()
isRestrictedView?: boolean;

View File

@ -43,7 +43,7 @@ export class UserService {
include: {
User: true
},
orderBy: { User: { alias: 'asc' } },
orderBy: { alias: 'asc' },
where: { GranteeUser: { id } }
});
let tags = await this.tagService.getByUser(id);
@ -98,7 +98,6 @@ export class UserService {
const {
accessToken,
Account,
alias,
authChallenge,
createdAt,
id,
@ -116,7 +115,6 @@ export class UserService {
const user: UserWithSettings = {
accessToken,
Account,
alias,
authChallenge,
createdAt,
id,

View File

@ -17,7 +17,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
imports: [
BullModule.registerQueue({
limiter: {
duration: ms('5 seconds'),
duration: ms('4 seconds'),
max: 1
},
name: DATA_GATHERING_QUEUE

View File

@ -6,7 +6,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
extractNumberFromString,
getYesterday
} from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@ -16,8 +20,6 @@ import { addDays, format, isBefore } from 'date-fns';
@Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface {
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
public constructor(
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
@ -77,7 +79,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
const html = await get();
const $ = cheerio.load(html);
const value = this.extractNumberFromString($(selector).text());
const value = extractNumberFromString($(selector).text());
return {
[symbol]: {
@ -175,15 +177,4 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return { items };
}
private extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(
GhostfolioScraperApiService.NUMERIC_REGEXP
);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
}

View File

@ -7,7 +7,7 @@ import { Module } from '@nestjs/common';
@Module({
exports: [TwitterBotService],
imports: [BenchmarkModule, ConfigurationModule, PropertyModule, SymbolModule],
imports: [BenchmarkModule, ConfigurationModule, SymbolModule],
providers: [TwitterBotService]
})
export class TwitterBotModule {}

View File

@ -1,9 +1,7 @@
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
PROPERTY_BENCHMARKS,
ghostfolioFearAndGreedIndexDataSource,
ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config';
@ -11,7 +9,6 @@ import {
resolveFearAndGreedIndex,
resolveMarketCondition
} from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { isWeekend } from 'date-fns';
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
@ -23,7 +20,6 @@ export class TwitterBotService {
public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService,
private readonly propertyService: PropertyService,
private readonly symbolService: SymbolService
) {
this.twitterClient = new TwitterApi({
@ -82,14 +78,9 @@ export class TwitterBotService {
}
private async getBenchmarkListing(aMax: number) {
const benchmarkAssets: UniqueAsset[] =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as UniqueAsset[]) ?? [];
const benchmarks = await this.benchmarkService.getBenchmarks(
benchmarkAssets
);
const benchmarks = await this.benchmarkService.getBenchmarks({
useCache: false
});
const benchmarkListing: string[] = [];

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default {
displayName: 'client',

View File

@ -1,4 +1,5 @@
<div class="align-items-center d-flex flex-row">
<div class="position-relative">
<div class="align-items-center d-flex flex-row" [hidden]="!fearAndGreedIndex">
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div>
<div class="h4 mb-0">
@ -11,3 +12,12 @@
<small class="d-block" i18n>Current Market Mood</small>
</div>
</div>
<ngx-skeleton-loader
*ngIf="!fearAndGreedIndex"
animation="pulse"
class="position-absolute w-100"
[theme]="{
height: '100%'
}"
></ngx-skeleton-loader>
</div>

View File

@ -1,3 +1,8 @@
:host {
display: block;
ngx-skeleton-loader {
bottom: 0;
top: 0;
}
}

View File

@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { FearAndGreedIndexComponent } from './fear-and-greed-index.component';
@NgModule({
declarations: [FearAndGreedIndexComponent],
exports: [FearAndGreedIndexComponent],
imports: [CommonModule]
imports: [CommonModule, NgxSkeletonLoaderModule]
})
export class GfFearAndGreedIndexModule {}

View File

@ -20,7 +20,6 @@
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</div>
</div>

View File

@ -106,7 +106,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.isLoadingPerformance = true;
this.dataService
.fetchChart({ range: this.dateRange })
.fetchChart({
range: this.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {

View File

@ -15,7 +15,6 @@
<gf-line-chart
class="position-absolute"
symbol="Performance"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems"
[hidden]="historicalDataItems?.length === 0"
[locale]="user?.settings?.locale"
@ -24,6 +23,7 @@
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
[unit]="user?.settings?.isExperimentalFeatures ? '%' : user?.settings?.baseCurrency"
></gf-line-chart>
</div>
</div>

View File

@ -249,10 +249,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private getTooltipPluginConfiguration() {
return {
...getTooltipOptions(
this.isInPercent ? undefined : this.currency,
this.isInPercent ? undefined : this.locale
),
...getTooltipOptions({
locale: this.isInPercent ? undefined : this.locale,
unit: this.isInPercent ? undefined : this.currency
}),
mode: 'index',
position: <unknown>'top',
xAlign: 'center',

View File

@ -23,13 +23,13 @@
class="mb-4"
benchmarkLabel="Average Unit Price"
[benchmarkDataItems]="benchmarkDataItems"
[currency]="SymbolProfile?.currency"
[historicalDataItems]="historicalDataItems"
[locale]="data.locale"
[showGradient]="true"
[showXAxis]="true"
[showYAxis]="true"
[symbol]="data.symbol"
[unit]="SymbolProfile?.currency"
></gf-line-chart>
<div class="row">

View File

@ -226,6 +226,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
});
}
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
this.dataService
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onRedeemCoupon() {
let couponCode = prompt($localize`Please enter your coupon code:`);
couponCode = couponCode?.trim();

View File

@ -119,6 +119,7 @@
<mat-form-field appearance="outline" class="w-100">
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)"
>
@ -187,6 +188,22 @@
></mat-slide-toggle>
</div>
</div>
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="align-items-center d-flex mt-4 py-1"
>
<div class="pr-1 w-50">
<div i18n>Experimental Features</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
[checked]="user.settings.isExperimentalFeatures"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onExperimentalFeaturesChange($event)"
></mat-slide-toggle>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div>

View File

@ -84,7 +84,7 @@
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
href="'../en/blog/2022/01/ghostfolio-first-months-in-open-source"
href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">

View File

@ -185,8 +185,8 @@ export class DataService {
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
}
public fetchChart({ range }: { range: DateRange }) {
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
public fetchChart({ range, version }: { range: DateRange; version: number }) {
return this.http.get<PortfolioChart>(`/api/v${version}/portfolio/chart`, {
params: { range }
});
}

View File

@ -6,70 +6,70 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/demo</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/faq</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/features</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/markets</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/register</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources</loc>
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
</url>
</urlset>

View File

@ -642,7 +642,7 @@
<target state="translated">Aktuelle Marktstimmung</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
<context context-type="linenumber">11</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
@ -1058,7 +1058,7 @@
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
@ -1262,7 +1262,7 @@
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context>
<context context-type="linenumber">248</context>
</context-group>
</trans-unit>
<trans-unit id="4420880039966769543" datatype="html">
@ -1270,7 +1270,7 @@
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="4819099731531004979" datatype="html">
@ -1278,7 +1278,7 @@
<target state="translated">Gutscheincode wurde eingelöst</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context>
<context context-type="linenumber">270</context>
</context-group>
</trans-unit>
<trans-unit id="7967484035994732534" datatype="html">
@ -1286,7 +1286,7 @@
<target state="translated">Neu laden</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context>
<context context-type="linenumber">271</context>
</context-group>
</trans-unit>
<trans-unit id="7963559562180316948" datatype="html">
@ -1294,7 +1294,7 @@
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context>
<context context-type="linenumber">317</context>
</context-group>
</trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1382,7 +1382,7 @@
<target state="new">Locale</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
@ -1390,7 +1390,7 @@
<target state="translated">Datums- und Zahlenformat</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
@ -1398,7 +1398,7 @@
<target state="translated">Ansicht</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context>
<context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
@ -1406,7 +1406,7 @@
<target state="translated">Einloggen mit Fingerabdruck</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context>
<context context-type="linenumber">181</context>
</context-group>
</trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
@ -1414,7 +1414,7 @@
<target state="translated">Benutzer ID</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context>
<context context-type="linenumber">208</context>
</context-group>
</trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
@ -1422,7 +1422,7 @@
<target state="translated">Zugangsberechtigung</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">217</context>
</context-group>
</trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -2641,6 +2641,14 @@
<context context-type="linenumber">4,7</context>
</context-group>
</trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<target state="translated">Experimentelle Funktionen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -583,7 +583,7 @@
<source>Current Market Mood</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
<context context-type="linenumber">11</context>
<context context-type="linenumber">12</context>
</context-group>
</trans-unit>
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
@ -957,7 +957,7 @@
<source>Please enter the amount of your emergency fund:</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
<context context-type="linenumber">48</context>
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
@ -1137,35 +1137,35 @@
<source>Please enter your coupon code:</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">230</context>
<context context-type="linenumber">248</context>
</context-group>
</trans-unit>
<trans-unit id="4420880039966769543" datatype="html">
<source>Could not redeem coupon code</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="4819099731531004979" datatype="html">
<source>Coupon code has been redeemed</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">252</context>
<context context-type="linenumber">270</context>
</context-group>
</trans-unit>
<trans-unit id="7967484035994732534" datatype="html">
<source>Reload</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">253</context>
<context context-type="linenumber">271</context>
</context-group>
</trans-unit>
<trans-unit id="7963559562180316948" datatype="html">
<source>Do you really want to remove this sign in method?</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
<context context-type="linenumber">299</context>
<context context-type="linenumber">317</context>
</context-group>
</trans-unit>
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
@ -1243,42 +1243,42 @@
<source>Locale</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
<source>Date and number format</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
<source>View Mode</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">159</context>
<context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
<source>Sign in with fingerprint</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">180</context>
<context context-type="linenumber">181</context>
</context-group>
</trans-unit>
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
<source>User ID</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">191</context>
<context context-type="linenumber">208</context>
</context-group>
</trans-unit>
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
<source>Granted Access</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">217</context>
</context-group>
</trans-unit>
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
@ -2359,6 +2359,13 @@
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
<source>Experimental Features</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
<context context-type="linenumber">196</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default {
displayName: 'common',

View File

@ -2,7 +2,13 @@ import { Chart, TooltipPosition } from 'chart.js';
import { getBackgroundColor, getTextColor } from './helper';
export function getTooltipOptions(currency = '', locale = '') {
export function getTooltipOptions({
locale = '',
unit = ''
}: {
locale?: string;
unit?: string;
} = {}) {
return {
backgroundColor: getBackgroundColor(),
bodyColor: `rgb(${getTextColor()})`,
@ -15,11 +21,11 @@ export function getTooltipOptions(currency = '', locale = '') {
label += ': ';
}
if (context.parsed.y !== null) {
if (currency) {
if (unit) {
label += `${context.parsed.y.toLocaleString(locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
})} ${currency}`;
})} ${unit}`;
} else {
label += context.parsed.y.toFixed(2);
}

View File

@ -6,6 +6,8 @@ import { de } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark } from './interfaces';
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
export function capitalize(aString: string) {
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
}
@ -43,6 +45,15 @@ export function encodeDataSource(aDataSource: DataSource) {
return Buffer.from(aDataSource, 'utf-8').toString('hex');
}
export function extractNumberFromString(aString: string): number {
try {
const [numberString] = aString.match(NUMERIC_REGEXP);
return parseFloat(numberString.trim());
} catch {
return undefined;
}
}
export function getBackgroundColor() {
return getCssVariable(
window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@ -2,6 +2,7 @@ import { ViewMode } from '@prisma/client';
export interface UserSettings {
baseCurrency?: string;
isExperimentalFeatures?: boolean;
isRestrictedView?: boolean;
language?: string;
locale: string;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
export default {
displayName: 'ui',

View File

@ -47,7 +47,6 @@ import { LineChartItem } from './interfaces/line-chart.interface';
export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() benchmarkDataItems: LineChartItem[] = [];
@Input() benchmarkLabel = '';
@Input() currency: string;
@Input() historicalDataItems: LineChartItem[];
@Input() locale: string;
@Input() showGradient = false;
@ -56,6 +55,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() showXAxis = false;
@Input() showYAxis = false;
@Input() symbol: string;
@Input() unit: string;
@Input() yMax: number;
@Input() yMaxLabel: string;
@Input() yMin: number;
@ -259,7 +259,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
private getTooltipPluginConfiguration() {
return {
...getTooltipOptions(this.currency, this.locale),
...getTooltipOptions({ locale: this.locale, unit: this.unit }),
mode: 'index',
position: <unknown>'top',
xAlign: 'center',

View File

@ -349,7 +349,7 @@ export class PortfolioProportionChartComponent
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) {
return {
...getTooltipOptions(this.baseCurrency, this.locale),
...getTooltipOptions({ locale: this.locale, unit: this.baseCurrency }),
callbacks: {
label: (context) => {
const labelIndex =

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.184.1",
"version": "1.187.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -53,16 +53,16 @@
"workspace-generator": "nx workspace-generator"
},
"dependencies": {
"@angular/animations": "14.1.0",
"@angular/cdk": "14.1.0",
"@angular/common": "14.1.0",
"@angular/compiler": "14.1.0",
"@angular/core": "14.1.0",
"@angular/forms": "14.1.0",
"@angular/material": "14.1.0",
"@angular/platform-browser": "14.1.0",
"@angular/platform-browser-dynamic": "14.1.0",
"@angular/router": "14.1.0",
"@angular/animations": "14.2.0",
"@angular/cdk": "14.2.0",
"@angular/common": "14.2.0",
"@angular/compiler": "14.2.0",
"@angular/core": "14.2.0",
"@angular/forms": "14.2.0",
"@angular/material": "14.2.0",
"@angular/platform-browser": "14.2.0",
"@angular/platform-browser-dynamic": "14.2.0",
"@angular/router": "14.2.0",
"@codewithdan/observable-store": "2.2.11",
"@dfinity/agent": "0.12.1",
"@dfinity/auth-client": "0.12.1",
@ -80,7 +80,7 @@
"@nestjs/platform-express": "9.0.7",
"@nestjs/schedule": "2.1.0",
"@nestjs/serve-static": "3.0.0",
"@nrwl/angular": "14.5.1",
"@nrwl/angular": "14.6.4",
"@prisma/client": "4.1.1",
"@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1",
@ -121,34 +121,34 @@
"passport-jwt": "4.0.0",
"prisma": "4.1.1",
"reflect-metadata": "0.1.13",
"rxjs": "7.4.0",
"rxjs": "7.5.6",
"stripe": "8.199.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.10.3",
"uuid": "8.3.2",
"yahoo-finance2": "2.3.3",
"zone.js": "0.11.6"
"zone.js": "0.11.8"
},
"devDependencies": {
"@angular-devkit/build-angular": "14.1.0",
"@angular-eslint/eslint-plugin": "14.0.2",
"@angular-eslint/eslint-plugin-template": "14.0.2",
"@angular-eslint/template-parser": "14.0.2",
"@angular/cli": "14.1.0",
"@angular/compiler-cli": "14.1.0",
"@angular/language-service": "14.1.0",
"@angular/localize": "14.1.0",
"@angular-devkit/build-angular": "14.2.1",
"@angular-eslint/eslint-plugin": "14.0.3",
"@angular-eslint/eslint-plugin-template": "14.0.3",
"@angular-eslint/template-parser": "14.0.3",
"@angular/cli": "14.2.1",
"@angular/compiler-cli": "14.2.0",
"@angular/language-service": "14.2.0",
"@angular/localize": "14.2.0",
"@nestjs/schematics": "9.0.1",
"@nestjs/testing": "9.0.7",
"@nrwl/cli": "14.5.1",
"@nrwl/cypress": "14.5.1",
"@nrwl/eslint-plugin-nx": "14.5.1",
"@nrwl/jest": "14.5.1",
"@nrwl/nest": "14.5.1",
"@nrwl/node": "14.5.1",
"@nrwl/nx-cloud": "14.2.0",
"@nrwl/storybook": "14.5.1",
"@nrwl/workspace": "14.5.1",
"@nrwl/cli": "14.6.4",
"@nrwl/cypress": "14.6.4",
"@nrwl/eslint-plugin-nx": "14.6.4",
"@nrwl/jest": "14.6.4",
"@nrwl/nest": "14.6.4",
"@nrwl/node": "14.6.4",
"@nrwl/nx-cloud": "14.6.1",
"@nrwl/storybook": "14.6.4",
"@nrwl/workspace": "14.6.4",
"@simplewebauthn/typescript-types": "5.2.1",
"@storybook/addon-essentials": "6.5.9",
"@storybook/angular": "6.5.9",
@ -160,9 +160,9 @@
"@types/cache-manager": "3.4.2",
"@types/color": "3.0.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "27.4.1",
"@types/jest": "28.1.8",
"@types/lodash": "4.14.174",
"@types/node": "16.11.7",
"@types/node": "18.7.1",
"@types/papaparse": "5.2.6",
"@types/passport-google-oauth20": "2.0.11",
"@typescript-eslint/eslint-plugin": "5.4.0",
@ -176,14 +176,15 @@
"import-sort-cli": "6.0.0",
"import-sort-parser-typescript": "6.0.0",
"import-sort-style-module": "6.0.0",
"jest": "27.5.1",
"jest-preset-angular": "11.1.2",
"nx": "14.5.1",
"jest": "28.1.3",
"jest-environment-jsdom": "28.1.1",
"jest-preset-angular": "12.2.2",
"nx": "14.6.4",
"prettier": "2.7.1",
"replace-in-file": "6.2.0",
"rimraf": "3.0.2",
"ts-jest": "27.1.4",
"ts-node": "10.8.1",
"ts-jest": "28.0.8",
"ts-node": "10.9.1",
"tslib": "2.0.0",
"typescript": "4.7.3"
},

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" DROP COLUMN "alias";

View File

@ -160,7 +160,6 @@ model Tag {
model User {
accessToken String?
alias String?
authChallenge String?
createdAt DateTime @default(now())
id String @id @default(uuid())

View File

@ -111,7 +111,6 @@ async function main() {
}
]
},
alias: 'Demo',
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
role: Role.DEMO
},

View File

@ -1,5 +0,0 @@
set -xe
echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest .
docker push ghostfolio/ghostfolio --all-tags

3080
yarn.lock

File diff suppressed because it is too large Load Diff