Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
0cee7a0b35 | |||
f3d337b044 | |||
7667af059c | |||
1095b47f45 | |||
dacd7271eb | |||
e093041184 | |||
8f2caa508a | |||
862f670ccf | |||
54bf4c7a43 |
21
CHANGELOG.md
21
CHANGELOG.md
@ -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.197.0 - 24.09.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added the value of the active filter in percentage on the allocations page
|
||||
- Extended the feature overview page by multi-language support (English, German, Italian)
|
||||
|
||||
### Changed
|
||||
|
||||
- Combined the performance and chart calculation
|
||||
- Improved the style of various selectors (density)
|
||||
|
||||
## 1.196.0 - 22.09.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Set up the language localization for Italiano (`it`)
|
||||
- Extended the landing page
|
||||
|
||||
## 1.195.0 - 20.09.2022
|
||||
|
||||
### Changed
|
||||
@ -195,7 +214,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
|
||||
- Set up language localization for German (`de`)
|
||||
- Set up the language localization for German (`de`)
|
||||
- Resolved the feature graphic of the blog post
|
||||
|
||||
### Changed
|
||||
|
@ -164,7 +164,7 @@ export class BenchmarkService {
|
||||
);
|
||||
|
||||
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
|
||||
return {
|
||||
const response = {
|
||||
marketData: [
|
||||
...marketDataItems
|
||||
.filter((marketDataItem, index) => {
|
||||
@ -181,17 +181,22 @@ export class BenchmarkService {
|
||||
marketDataItem.marketPrice
|
||||
) * 100
|
||||
};
|
||||
}),
|
||||
{
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
value:
|
||||
this.calculateChangeInPercentage(
|
||||
marketPriceAtStartDate,
|
||||
currentSymbolItem.marketPrice
|
||||
) * 100
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
if (currentSymbolItem?.marketPrice) {
|
||||
response.marketData.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
value:
|
||||
this.calculateChangeInPercentage(
|
||||
marketPriceAtStartDate,
|
||||
currentSymbolItem.marketPrice
|
||||
) * 100
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private getMarketCondition(aPerformanceInPercent: number) {
|
||||
|
@ -11,6 +11,7 @@ import { NextFunction, Request, Response } from 'express';
|
||||
export class FrontendMiddleware implements NestMiddleware {
|
||||
public indexHtmlDe = '';
|
||||
public indexHtmlEn = '';
|
||||
public indexHtmlIt = '';
|
||||
public isProduction: boolean;
|
||||
|
||||
public constructor(
|
||||
@ -32,6 +33,10 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
|
||||
'utf8'
|
||||
);
|
||||
this.indexHtmlIt = fs.readFileSync(
|
||||
this.getPathOfIndexHtmlFile('it'),
|
||||
'utf8'
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@ -61,6 +66,15 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
})
|
||||
);
|
||||
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
|
||||
res.send(
|
||||
this.interpolate(this.indexHtmlIt, {
|
||||
featureGraphicPath,
|
||||
languageCode: 'it',
|
||||
path: req.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
})
|
||||
);
|
||||
} else {
|
||||
res.send(
|
||||
this.interpolate(this.indexHtmlEn, {
|
||||
|
@ -272,23 +272,20 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const isInPercentage = true;
|
||||
|
||||
return Object.keys(totalNetPerformanceValues).map((date) => {
|
||||
return isInPercentage
|
||||
? {
|
||||
date,
|
||||
value: totalInvestmentValues[date].eq(0)
|
||||
? 0
|
||||
: totalNetPerformanceValues[date]
|
||||
.div(totalInvestmentValues[date])
|
||||
.mul(100)
|
||||
.toNumber()
|
||||
}
|
||||
: {
|
||||
date,
|
||||
value: totalNetPerformanceValues[date].toNumber()
|
||||
};
|
||||
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
|
||||
? 0
|
||||
: totalNetPerformanceValues[date]
|
||||
.div(totalInvestmentValues[date])
|
||||
.mul(100)
|
||||
.toNumber();
|
||||
|
||||
return {
|
||||
date,
|
||||
netPerformanceInPercentage,
|
||||
netPerformance: totalNetPerformanceValues[date].toNumber(),
|
||||
value: netPerformanceInPercentage
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,26 +110,6 @@ 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)
|
||||
@ -319,6 +299,35 @@ export class PortfolioController {
|
||||
return performanceInformation;
|
||||
}
|
||||
|
||||
@Get('performance')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
@Version('2')
|
||||
public async getPerformanceV2(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') dateRange
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const performanceInformation = await this.portfolioService.getPerformanceV2(
|
||||
{
|
||||
dateRange,
|
||||
impersonationId
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
performanceInformation.performance = nullifyValuesInObject(
|
||||
performanceInformation.performance,
|
||||
['currentGrossPerformance', 'currentNetPerformance', 'currentValue']
|
||||
);
|
||||
}
|
||||
|
||||
return performanceInformation;
|
||||
}
|
||||
|
||||
@Get('positions')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
|
@ -355,11 +355,14 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getChartV2(
|
||||
aImpersonationId: string,
|
||||
aDateRange: DateRange = 'max'
|
||||
): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
public async getChartV2({
|
||||
dateRange = 'max',
|
||||
impersonationId
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
impersonationId: string;
|
||||
}): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
@ -383,7 +386,7 @@ export class PortfolioService {
|
||||
const endDate = new Date();
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
|
||||
const daysInMarket = differenceInDays(new Date(), startDate);
|
||||
const step = Math.round(
|
||||
@ -987,6 +990,105 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getPerformanceV2({
|
||||
dateRange = 'max',
|
||||
impersonationId
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
impersonationId: string;
|
||||
}): Promise<PortfolioPerformanceResponse> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
userId
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
currentRateService: this.currentRateService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
|
||||
if (transactionPoints?.length <= 0) {
|
||||
return {
|
||||
chart: [],
|
||||
hasErrors: false,
|
||||
performance: {
|
||||
currentGrossPerformance: 0,
|
||||
currentGrossPerformancePercent: 0,
|
||||
currentNetPerformance: 0,
|
||||
currentNetPerformancePercent: 0,
|
||||
currentValue: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate
|
||||
);
|
||||
|
||||
const hasErrors = currentPositions.hasErrors;
|
||||
const currentValue = currentPositions.currentValue.toNumber();
|
||||
const currentGrossPerformance = currentPositions.grossPerformance;
|
||||
const currentGrossPerformancePercent =
|
||||
currentPositions.grossPerformancePercentage;
|
||||
let currentNetPerformance = currentPositions.netPerformance;
|
||||
let currentNetPerformancePercent =
|
||||
currentPositions.netPerformancePercentage;
|
||||
|
||||
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
|
||||
// // If algebraic sign is different, harmonize it
|
||||
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
|
||||
// }
|
||||
|
||||
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
|
||||
// // If algebraic sign is different, harmonize it
|
||||
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
|
||||
// }
|
||||
|
||||
const historicalDataContainer = await this.getChartV2({
|
||||
dateRange,
|
||||
impersonationId
|
||||
});
|
||||
|
||||
const itemOfToday = historicalDataContainer.items.find((item) => {
|
||||
return item.date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (itemOfToday) {
|
||||
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
||||
currentNetPerformancePercent = new Big(
|
||||
itemOfToday.netPerformanceInPercentage
|
||||
).div(100);
|
||||
}
|
||||
|
||||
return {
|
||||
chart: historicalDataContainer.items.map(
|
||||
({ date, netPerformanceInPercentage }) => {
|
||||
return {
|
||||
date,
|
||||
value: netPerformanceInPercentage
|
||||
};
|
||||
}
|
||||
),
|
||||
errors: currentPositions.errors,
|
||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||
performance: {
|
||||
currentValue,
|
||||
currentGrossPerformance: currentGrossPerformance.toNumber(),
|
||||
currentGrossPerformancePercent:
|
||||
currentGrossPerformancePercent.toNumber(),
|
||||
currentNetPerformance: currentNetPerformance.toNumber(),
|
||||
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||
const currency = this.request.user.Settings.settings.baseCurrency;
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
@ -2,7 +2,10 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form class="align-items-center d-flex" [formGroup]="filterForm">
|
||||
<mat-form-field appearance="outline" class="flex-grow-1">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline flex-grow-1 mr-2 without-hint"
|
||||
>
|
||||
<mat-select formControlName="status">
|
||||
<mat-option></mat-option>
|
||||
<mat-option
|
||||
@ -13,7 +16,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="ml-1"
|
||||
class="mt-1"
|
||||
color="warn"
|
||||
mat-flat-button
|
||||
(click)="onDeleteJobs()"
|
||||
|
@ -162,8 +162,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<form #couponForm="ngForm">
|
||||
<mat-form-field appearance="outline" class="mr-2">
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline mr-2 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="duration"
|
||||
[value]="couponDuration"
|
||||
@ -176,6 +179,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="mt-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
(click)="onAddCoupon()"
|
||||
|
@ -10,7 +10,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
||||
<mat-form-field appearance="outline" class="w-100" color="accent">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="w-100 without-hint"
|
||||
color="accent"
|
||||
>
|
||||
<mat-label i18n>Compare with...</mat-label>
|
||||
<mat-select
|
||||
name="benchmark"
|
||||
@ -26,7 +30,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="my-2 text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="isLoading"
|
||||
|
@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
!this.hasImpersonationId &&
|
||||
!this.user.settings.isRestrictedView &&
|
||||
this.user.settings.viewMode !== 'ZEN';
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeDateRange(dateRange: DateRange) {
|
||||
@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.historicalDataItems = null;
|
||||
this.isLoadingPerformance = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({
|
||||
.fetchPortfolioPerformance({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||
return {
|
||||
date: chartDataItem.date,
|
||||
value: chartDataItem.value
|
||||
};
|
||||
});
|
||||
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.errors = response.errors;
|
||||
this.hasError = response.hasErrors;
|
||||
this.performance = response.performance;
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures) {
|
||||
this.historicalDataItems = response.chart.map(({ date, value }) => {
|
||||
return {
|
||||
date,
|
||||
value
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.dataService
|
||||
.fetchChart({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: 1
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.chart.map(
|
||||
({ date, value }) => {
|
||||
return {
|
||||
date,
|
||||
value
|
||||
};
|
||||
}
|
||||
);
|
||||
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToUpdateViewMode: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public language = document.documentElement.lang;
|
||||
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
|
||||
public locales = ['de', 'de-CH', 'en-GB', 'en-US', 'it'];
|
||||
public price: number;
|
||||
public priceId: string;
|
||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||
|
@ -94,7 +94,10 @@
|
||||
<ng-container i18n>Base Currency</ng-container>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline w-100 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="baseCurrency"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -116,7 +119,10 @@
|
||||
<div class="hint-text text-muted" i18n>Beta</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline w-100 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="language"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -126,6 +132,7 @@
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option value="de">Deutsch</mat-option>
|
||||
<mat-option value="en">English</mat-option>
|
||||
<mat-option value="it">Italiano</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -138,7 +145,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline w-100 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="locale"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
@ -161,7 +171,10 @@
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<div class="align-items-center d-flex overflow-hidden">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="compact-with-outline w-100 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="viewMode"
|
||||
[disabled]="!hasPermissionToUpdateViewMode"
|
||||
|
@ -192,6 +192,17 @@
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
<h4>Multi-Language</h4>
|
||||
<p class="m-0">
|
||||
Use Ghostfolio in multiple languages: English, German and
|
||||
Italian are currently supported.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card class="d-flex flex-column h-100">
|
||||
<div class="flex-grow-1">
|
||||
|
@ -55,6 +55,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-3">
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card>
|
||||
<mat-card-title class="text-center">360° View</mat-card-title>
|
||||
Get the full picture of your personal finances across multiple
|
||||
platforms.
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card>
|
||||
<mat-card-title class="text-center">Web3 Ready</mat-card-title>
|
||||
Use Ghostfolio anonymously and own your financial data.
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card>
|
||||
<mat-card-title class="text-center">Open Source</mat-card-title>
|
||||
Benefit from continuous improvements through a strong community.
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
|
||||
@ -133,19 +155,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="row my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
How does <strong>Ghostfolio</strong> work?
|
||||
</h2>
|
||||
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
|
||||
<ol class="m-0 pl-3">
|
||||
<li class="mb-2">
|
||||
Sign up anonymously<br />(no e-mail address nor credit card required)
|
||||
</li>
|
||||
<li class="mb-2">Add any of your historical transactions</li>
|
||||
<li>Get valuable insights of your portfolio composition</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card class="d-flex flex-row h-100">
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-bold">Sign up anonymously*</div>
|
||||
<div class="text-muted">
|
||||
<small>* no e-mail address nor credit card required</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-2 text-muted text-right">1</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card class="d-flex flex-row h-100">
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-bold">
|
||||
Add any of your historical transactions
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-2 text-muted text-right">2</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-4 my-2">
|
||||
<mat-card class="d-flex flex-row h-100">
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-bold">
|
||||
Get valuable insights of your portfolio composition
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-2 text-muted text-right">3</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
|
||||
@ -14,6 +15,7 @@ import { LandingPageComponent } from './landing-page.component';
|
||||
GfLogoModule,
|
||||
LandingPageRoutingModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -13,8 +13,16 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Proportion of Net Worth</mat-card-title>
|
||||
<mat-card-header class="overflow-hidden w-100">
|
||||
<mat-card-title class="text-truncate" i18n
|
||||
>Proportion of Net Worth</mat-card-title
|
||||
>
|
||||
<gf-value
|
||||
class="align-items-end flex-grow-1 ml-2"
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
|
||||
></gf-value>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-progress-bar
|
||||
|
@ -20,6 +20,7 @@
|
||||
::ng-deep {
|
||||
.mat-card-header-text {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.isLoadingBenchmarkComparator = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
|
||||
.fetchPortfolioPerformance({
|
||||
range: this.user?.settings?.dateRange,
|
||||
version: 2
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());
|
||||
|
@ -353,12 +353,16 @@ export class DataService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchPortfolioPerformance(params: { [param: string]: any }) {
|
||||
public fetchPortfolioPerformance({
|
||||
range,
|
||||
version
|
||||
}: {
|
||||
range: DateRange;
|
||||
version: number;
|
||||
}) {
|
||||
return this.http.get<PortfolioPerformanceResponse>(
|
||||
'/api/v1/portfolio/performance',
|
||||
{
|
||||
params
|
||||
}
|
||||
`/api/v${version}/portfolio/performance`,
|
||||
{ params: { range } }
|
||||
);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -146,13 +146,6 @@ ngx-skeleton-loader {
|
||||
@include gf-table;
|
||||
}
|
||||
|
||||
.mat-fab,
|
||||
.mat-flat-button {
|
||||
&.mat-primary {
|
||||
color: rgba(var(--light-primary-text)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
&:not([class*='mat-elevation-z']) {
|
||||
border: 1px solid rgba(var(--dark-dividers));
|
||||
@ -164,6 +157,49 @@ ngx-skeleton-loader {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mat-fab,
|
||||
.mat-flat-button {
|
||||
&.mat-primary {
|
||||
color: rgba(var(--light-primary-text)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
&.compact-with-outline {
|
||||
.mat-form-field-wrapper {
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
.mat-form-field-infix {
|
||||
border-top-width: 0;
|
||||
padding: 1rem 0 0.75rem;
|
||||
|
||||
.mat-form-field-label {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.mat-select-arrow-wrapper {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-form-field-prefix {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.mat-form-field-suffix {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.without-hint {
|
||||
.mat-form-field-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-min-width {
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as currencies from '@dinero.js/currencies';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { de, it } from 'date-fns/locale';
|
||||
|
||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||
import { Benchmark } from './interfaces';
|
||||
@ -75,6 +75,8 @@ export function getCssVariable(aCssVariable: string) {
|
||||
export function getDateFnsLocale(aLanguageCode: string) {
|
||||
if (aLanguageCode === 'de') {
|
||||
return de;
|
||||
} else if (aLanguageCode === 'it') {
|
||||
return it;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -2,5 +2,7 @@ export interface HistoricalDataItem {
|
||||
averagePrice?: number;
|
||||
date: string;
|
||||
grossPerformancePercent?: number;
|
||||
netPerformance?: number;
|
||||
netPerformanceInPercentage?: number;
|
||||
value: number;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { HistoricalDataItem } from '../historical-data-item.interface';
|
||||
import { PortfolioPerformance } from '../portfolio-performance.interface';
|
||||
import { ResponseError } from './errors.interface';
|
||||
|
||||
export interface PortfolioPerformanceResponse extends ResponseError {
|
||||
chart?: HistoricalDataItem[];
|
||||
performance: PortfolioPerformance;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.historicalDataItems) {
|
||||
if (this.historicalDataItems || this.historicalDataItems === null) {
|
||||
setTimeout(() => {
|
||||
// Wait for the chartCanvas
|
||||
this.initialize();
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.195.0",
|
||||
"version": "1.197.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
Reference in New Issue
Block a user