Compare commits

...

10 Commits

Author SHA1 Message Date
cc16ba5dc8 Release 1.169.0 (#1077) 2022-07-14 16:31:03 +02:00
d10227bc39 Feature/add support for luna2 and songbird cryptocurrencies (#1075)
* Add LUNA2 and SGB1

* Update changelog
2022-07-14 16:28:50 +02:00
4e214c32e8 Feature/update cryptocurrencies.json 20220714 (#1074)
* Update cryptocurrencies.json

* Update changelog
2022-07-14 16:27:32 +02:00
49e2862e03 Feature/add blog post about personal finances (#1073)
* Add blog post

* Update changelog
2022-07-14 16:16:07 +02:00
34e33a2400 Feature/upgrade date fns to version 2.28.0 (#1070)
* Upgrade date-fns

* Update changelog
2022-07-11 19:39:03 +02:00
ec9bc984af Release 1.168.0 (#1071) 2022-07-10 22:25:58 +02:00
2388c494df Feature/handle currency pair inconsistency in yahoo finance service (#1069)
* Handle occasional currency pair inconsistency: GBP=X instead of USDGBP=X

* Update changelog
2022-07-10 22:24:27 +02:00
d71ab10eed Bugfix/fix content height of account detail dialog (#1068)
* Fix height

* Update changelog
2022-07-10 21:44:23 +02:00
0e0592180f Add current month (#1067) 2022-07-10 09:41:48 +02:00
60e2aff488 Extend investment timeline by month (#1066)
* Extend investment timeline grouped by month

* Update changelog
2022-07-09 21:18:05 +02:00
30 changed files with 782 additions and 947 deletions

View File

@ -5,6 +5,33 @@ 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.169.0 - 14.07.2022
### Added
- Added support for the cryptocurrency _Songbird_ (`SGB1-USD`)
- Added support for the cryptocurrency _Terra 2.0_ (`LUNA2-USD`)
- Added a blog post
### Changed
- Refreshed the cryptocurrencies list to support more coins by default
- Upgraded `date-fns` from version `2.22.1` to `2.28.0`
## 1.168.0 - 10.07.2022
### Added
- Extended the investment timeline grouped by month
### Changed
- Handled an occasional currency pair inconsistency in the _Yahoo Finance_ service (`GBP=X` instead of `USDGBP=X`)
### Fixed
- Fixed the content height of the account detail dialog
## 1.167.0 - 07.07.2022
### Added

View File

@ -14,8 +14,11 @@ import {
format,
isAfter,
isBefore,
isSameMonth,
isSameYear,
max,
min
min,
set
} from 'date-fns';
import { first, flatten, isNumber, sortBy } from 'lodash';
@ -323,6 +326,46 @@ export class PortfolioCalculator {
});
}
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
if (this.orders.length === 0) {
return [];
}
const investments = [];
let currentDate = parseDate(this.orders[0].date);
let investmentByMonth = new Big(0);
for (const [index, order] of this.orders.entries()) {
if (
isSameMonth(parseDate(order.date), currentDate) &&
isSameYear(parseDate(order.date), currentDate)
) {
investmentByMonth = investmentByMonth.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
if (index === this.orders.length - 1) {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
}
} else {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
currentDate = parseDate(order.date);
investmentByMonth = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
}
return investments;
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string

View File

@ -20,7 +20,12 @@ import {
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type {
DateRange,
GroupBy,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Controller,
Get,
@ -217,7 +222,8 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string
@Headers('impersonation-id') impersonationId: string,
@Query('groupBy') groupBy?: GroupBy
): Promise<PortfolioInvestments> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
@ -229,9 +235,16 @@ export class PortfolioController {
);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
let investments: InvestmentItem[];
if (groupBy === 'month') {
investments = await this.portfolioService.getInvestments(
impersonationId,
'month'
);
} else {
investments = await this.portfolioService.getInvestments(impersonationId);
}
if (
impersonationId ||

View File

@ -41,6 +41,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
import type {
AccountWithValue,
DateRange,
GroupBy,
Market,
OrderWithAccount,
RequestWithUser
@ -64,6 +65,7 @@ import {
max,
parse,
parseISO,
set,
setDayOfYear,
startOfDay,
subDays,
@ -183,7 +185,8 @@ export class PortfolioService {
}
public async getInvestments(
aImpersonationId: string
aImpersonationId: string,
groupBy?: GroupBy
): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
@ -204,28 +207,57 @@ export class PortfolioService {
return [];
}
const investments = portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
let investments: InvestmentItem[];
// Add investment of today
const investmentOfToday = investments.filter((investment) => {
return investment.date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter((investment) => {
return isBefore(parseDate(investment.date), new Date());
if (groupBy === 'month') {
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
// Add investment of current month
const dateOfCurrentMonth = format(
set(new Date(), { date: 1 }),
DATE_FORMAT
);
const investmentOfCurrentMonth = investments.filter(({ date }) => {
return date === dateOfCurrentMonth;
});
if (investmentOfCurrentMonth.length <= 0) {
investments.push({
date: dateOfCurrentMonth,
investment: 0
});
}
} else {
investments = portfolioCalculator
.getInvestments()
.map(({ date, investment }) => {
return {
date,
investment: investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter(({ date }) => {
return isBefore(parseDate(date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
}
return sortBy(investments, (investment) => {

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
{
"LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",
"UNI1": "Uniswap"
}

View File

@ -37,10 +37,15 @@ export class YahooFinanceService implements DataProviderInterface {
}
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace(
let symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
);
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol.replace('=X', '');
}

View File

@ -78,6 +78,13 @@ const routes: Routes = [
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule)
},
{
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () =>
import(
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
},
{
path: 'features',
loadChildren: () =>

View File

@ -1,3 +1,7 @@
:host {
display: block;
.mat-dialog-content {
max-height: unset;
}
}

View File

@ -22,7 +22,10 @@ import {
transformTickToAbbreviation
} from '@ghostfolio/common/helper';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy } from '@ghostfolio/common/types';
import {
BarController,
BarElement,
Chart,
LineController,
LineElement,
@ -42,6 +45,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() currency: string;
@Input() daysInMarket: number;
@Input() groupBy: GroupBy;
@Input() investments: InvestmentItem[];
@Input() isInPercent = false;
@Input() locale: string;
@ -53,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
public constructor() {
Chart.register(
BarController,
BarElement,
LinearScale,
LineController,
LineElement,
@ -78,7 +84,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private initialize() {
this.isLoading = true;
if (this.investments?.length > 0) {
if (!this.groupBy && this.investments?.length > 0) {
// Extend chart by 5% of days in market (before)
const firstItem = this.investments[0];
this.investments.unshift({
@ -102,13 +108,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
const data = {
labels: this.investments.map((position) => {
return position.date;
labels: this.investments.map((investmentItem) => {
return investmentItem.date;
}),
datasets: [
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2,
borderWidth: this.groupBy ? 0 : 2,
data: this.investments.map((position) => {
return position.investment;
}),
@ -137,6 +144,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
this.chart = new Chart(this.chartCanvas.nativeElement, {
data,
options: {
animation: false,
elements: {
line: {
tension: 0
@ -178,8 +186,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
grid: {
borderColor: `rgba(${getTextColor()}, 0.1)`,
color: `rgba(${getTextColor()}, 0.8)`,
display: false
display: false,
drawBorder: false
},
position: 'right',
ticks: {
callback: (value: number) => {
return transformTickToAbbreviation(value);
@ -192,12 +202,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
},
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
type: 'line'
type: this.groupBy ? 'bar' : 'line'
});
this.isLoading = false;
}
}
this.isLoading = false;
}
private getTooltipPluginConfiguration() {

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
<h1 class="mb-1">Hallo Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div>
</div>
<section class="mb-4">

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
<h1 class="mb-1">Hello Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div>
</div>
<section class="mb-4">

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>
<h1 class="mb-1">
👻 Ghostfolio
<span class="text-nowrap">First months in Open Source</span>
</h1>

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
const routes: Routes = [
{
path: '',
component: HowDoIGetMyFinancesInOrderPageComponent,
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HowDoIGetMyFinancesInOrderRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-how-do-i-get-my-finances-in-order-page',
styleUrls: ['./how-do-i-get-my-finances-in-order-page.scss'],
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
})
export class HowDoIGetMyFinancesInOrderPageComponent {}

View File

@ -0,0 +1,206 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">How do I get my finances in order?</h1>
<div class="text-muted"><small>14.07.2022</small></div>
</div>
<section class="mb-4">
<p>
Before you can think of
<a [routerLink]="['/resources']">long-term investing</a>, you need
to have your finances in order. Take a look at Peter's journey to
see how you can achieve it, too.
</p>
<p>
Peter enjoys life, but sometimes he overspends a bit. He realizes it
when money runs out already in the middle of the month. Then the
next few days become difficult and saving money is out of the
question. That is why he wants to plan his monthly budget in the
future.
</p>
<p>
Peter has a decent salary in his job. But as soon as the salary
arrives in his account, it melts away. In order to find out where
his money is disappearing, he has decided to plan his monthly
budget. He wants to be able to put money aside for major expenses
and set financial goals.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Keeping a traditional or digital budget book</h2>
<p>
First, Peter obtains an overview of his personal finances. To do so,
he starts keeping a budget book. This can be done on paper by
listing his income and expenses for a few months, or he can create a
simple spreadsheet in Excel. In addition, many credit card providers
offer the feature within their apps of having expenses automatically
analyzed according to different categories. According to the
<a href="https://www.bfs.admin.ch/bfs/en/home.html"
>Swiss Federal Statistical Office</a
>, households in Switzerland spend around 20 percent of their
disposable income on housing and around 10 percent on groceries.
</p>
<p>
With the smartphone app, Peter has a better overview of his
financial affairs. The application assigns the bookings to
individual categories. Peter can assign specific budgets to each of
them. This way, he is always informed about how much money he can
still spend on restaurant visits in the current month, for example.
A traditional method is the so-called
<a
href="https://www.investopedia.com/envelope-budgeting-system-5208026"
>envelope method</a
>. One envelope is labeled for each category like groceries, rent or
student loans. The monthly budget is put into the envelopes in cash.
Many apps offer the same budgeting system in a more convenient,
virtual way.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Planning and investing</h2>
<p>
If Peter has spent less money than planned on eating out at
restaurants, he can set aside the remaining amount. This way, he can
treat himself to something special every now and then. From now on,
he saves a fixed amount of money in a separate account ("pay
yourself first") by standing order at the beginning of the month. As
soon as there are three net monthly salaries in the account, he
invests the monthly savings amount in a passively managed global
equity fund. This grows his assets over the years and allows him to
supplement his pension later.
</p>
</section>
<section class="mb-4">
<h2 class="h4">How to achieve your financial goals?</h2>
<p>
If you follow these five actionable tips, you can reach your
financial goals easier and faster. Start with one tip and when you
implement it well, you can try the next one to ultimately have more
money at the end of the month.
</p>
<h3 class="h5">1. Visualize your goals</h3>
<p>
Start visualizing your goals. For example, hang up pictures of the
travel destination you are saving for. Imagine that you have already
achieved the goal to slowly adapt your mindset.
</p>
<h3 class="h5">2. Write off personal items</h3>
<p>
Do as a business does and write off purchases annually. For a new
car, you could set aside one-sixth of the purchase price each year.
</p>
<h3 class="h5">3. Save at the beginning of the month</h3>
<p>
Have a savings amount deducted from your account at the beginning of
the month. Then you will pay yourself first and spend less money.
</p>
<h3 class="h5">4. Follow the 50-30-20 rule</h3>
<p>
You need 50 percent of your disposable income for fixed costs. 30
percent can be spent on personal needs such as hobbies, travel or
consumer electronics. 20 percent is left for savings or to pay off
potential debts.
</p>
<h3 class="h5">5. Track your progress</h3>
<p>
If you have any money to spare, invest it in a broadly diversified,
low-cost portfolio excluding the risks of individual stocks. Track
the progress of your portfolio and net worth with
<a href="https://ghostfol.io">Ghostfolio</a>, a web-based personal
finance management software.
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Assets</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Budget</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cash</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Debt</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Equity</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">ETF</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Expense</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fund</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Goal</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Income</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Money</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Net Worth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Pension</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Planning</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Salary</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Saving</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Spreadsheet</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Stock</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Strategy</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HowDoIGetMyFinancesInOrderRoutingModule } from './how-do-i-get-my-finances-in-order-page-routing.module';
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
@NgModule({
declarations: [HowDoIGetMyFinancesInOrderPageComponent],
imports: [
CommonModule,
HowDoIGetMyFinancesInOrderRoutingModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HowDoIGetMyFinancesInOrderPageModule {}

View File

@ -2,10 +2,36 @@
<div class="mb-5 row">
<div class="col">
<h3 class="mb-3 text-center" i18n>Blog</h3>
<mat-card class="blog-container">
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap mb-3 no-gutters row">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
How do I get my finances in order?
</div>
<div class="d-flex text-muted">14.07.2022</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
@ -25,7 +51,13 @@
</div>
</a>
</div>
<div class="flex-nowrap mb-3 no-gutters row">
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
@ -43,6 +75,12 @@
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"

View File

@ -4,6 +4,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
import { differenceInDays } from 'date-fns';
import { sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -22,6 +23,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public investments: InvestmentItem[];
public investmentsByMonth: InvestmentItem[];
public mode: GroupBy;
public modeOptions: ToggleOption[] = [
{ label: 'Monthly', value: 'month' },
{ label: 'Accumulating', value: undefined }
];
public top3: Position[];
public user: User;
@ -55,6 +62,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchInvestmentsByMonth()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ investments }) => {
this.investmentsByMonth = investments;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPositions({ range: 'max' })
.pipe(takeUntil(this.unsubscribeSubject))
@ -86,6 +102,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
}
public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

View File

@ -2,8 +2,19 @@
<div class="investment-chart row">
<div class="col-lg">
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
<div class="mb-3">
<div class="h5 mb-3" i18n>Investment Timeline</div>
<div class="mb-4">
<div class="align-items-center d-flex mb-4">
<div class="flex-grow-1 h5 mb-0 text-truncate" i18n>
Investment Timeline
</div>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<gf-investment-chart
class="h-100"
[currency]="user?.settings?.baseCurrency"
@ -11,6 +22,17 @@
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': mode }"
></gf-investment-chart>
<gf-investment-chart
class="h-100"
groupBy="month"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investmentsByMonth"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': !mode }"
></gf-investment-chart>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -14,6 +15,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
AnalysisPageRoutingModule,
CommonModule,
GfInvestmentChartModule,
GfToggleModule,
GfValueModule,
MatCardModule,
NgxSkeletonLoaderModule

View File

@ -204,6 +204,22 @@ export class DataService {
);
}
public fetchInvestmentsByMonth(): Observable<PortfolioInvestments> {
return this.http
.get<any>('/api/v1/portfolio/investments', {
params: { groupBy: 'month' }
})
.pipe(
map((response) => {
if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate);
}
return response;
})
);
}
public fetchSymbolItem({
dataSource,
includeHistoricalData,

View File

@ -6,54 +6,58 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about/changelog</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/blog</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/demo</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00: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-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/features</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/markets</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pricing</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/register</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/resources</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
<lastmod>2022-07-14T00:00:00+00:00</lastmod>
</url>
</urlset>

View File

@ -43,7 +43,7 @@ export function getTooltipPositionerMapTop(
chart: Chart,
position: TooltipPosition
) {
if (!position) {
if (!position || !chart?.chartArea) {
return false;
}
return {

View File

@ -0,0 +1 @@
export type GroupBy = 'month';

View File

@ -2,7 +2,8 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
import { AccountWithValue } from './account-with-value.type';
import type { DateRange } from './date-range.type';
import type { Granularity } from './granularity.type';
import { MarketState } from './market-state-type';
import { GroupBy } from './group-by.type';
import { MarketState } from './market-state.type';
import { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.type';
@ -13,6 +14,7 @@ export type {
AccountWithValue,
DateRange,
Granularity,
GroupBy,
Market,
MarketState,
OrderWithAccount,

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.167.0",
"version": "1.169.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -93,7 +93,7 @@
"color": "4.0.1",
"countries-list": "2.6.1",
"countup.js": "2.0.7",
"date-fns": "2.22.1",
"date-fns": "2.28.0",
"envalid": "7.3.1",
"google-spreadsheet": "3.2.0",
"http-status-codes": "2.2.0",

View File

@ -7710,10 +7710,10 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
date-fns@2.28.0:
version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
date-fns@^1.27.2:
version "1.30.1"