Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
bd4608e521 | |||
0d8362ca8f | |||
638ae3f7fa | |||
6e7cf0380b | |||
ec2ecab751 | |||
598fe41b8c | |||
ba7c98d325 | |||
65e062ad26 | |||
8526b5a027 |
25
CHANGELOG.md
25
CHANGELOG.md
@ -5,6 +5,31 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.138.0 - 16.04.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to export a single future activity (draft) as an `.ics` file
|
||||||
|
- Added the _Boringly Getting Rich_ guide to the resources section
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Separated the deposit and savings in the chart of the the _FIRE_ calculator
|
||||||
|
|
||||||
|
## 1.137.0 - 15.04.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to export future activities (drafts) as an `.ics` file
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrated the search functionality to `yahoo-finance2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the average price / investment calculation for sell activities
|
||||||
|
|
||||||
## 1.136.0 - 13.04.2022
|
## 1.136.0 - 13.04.2022
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -42,6 +42,7 @@ export class ExportService {
|
|||||||
accountId,
|
accountId,
|
||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
|
id,
|
||||||
quantity,
|
quantity,
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
type,
|
type,
|
||||||
@ -49,13 +50,14 @@ export class ExportService {
|
|||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
accountId,
|
accountId,
|
||||||
date,
|
|
||||||
fee,
|
fee,
|
||||||
|
id,
|
||||||
quantity,
|
quantity,
|
||||||
type,
|
type,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
currency: SymbolProfile.currency,
|
currency: SymbolProfile.currency,
|
||||||
dataSource: SymbolProfile.dataSource,
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
date: date.toISOString(),
|
||||||
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,13 @@ function mockGetValue(symbol: string, date: Date) {
|
|||||||
|
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
|
case 'NOVN.SW':
|
||||||
|
if (isSameDay(parseDate('2022-04-11'), date)) {
|
||||||
|
return { marketPrice: 87.8 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { marketPrice: 0 };
|
return { marketPrice: 0 };
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import { CurrentRateServiceMock } from './current-rate.service.mock';
|
||||||
|
import { PortfolioCalculator } from './portfolio-calculator';
|
||||||
|
|
||||||
|
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||||
|
return CurrentRateServiceMock;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PortfolioCalculator', () => {
|
||||||
|
let currentRateService: CurrentRateService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentRateService = new CurrentRateService(null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get current positions', () => {
|
||||||
|
it.only('with BALN.SW buy and sell', async () => {
|
||||||
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
currentRateService,
|
||||||
|
currency: 'CHF',
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-03-07',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(1.3),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(2),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: new Big(75.8)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'CHF',
|
||||||
|
date: '2022-04-08',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
fee: new Big(2.95),
|
||||||
|
name: 'Novartis AG',
|
||||||
|
quantity: new Big(1),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
type: 'SELL',
|
||||||
|
unitPrice: new Big(85.73)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
portfolioCalculator.computeTransactionPoints();
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(Date, 'now')
|
||||||
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
parseDate('2022-03-07')
|
||||||
|
);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
|
||||||
|
expect(currentPositions).toEqual({
|
||||||
|
currentValue: new Big('87.8'),
|
||||||
|
errors: [],
|
||||||
|
grossPerformance: new Big('21.93'),
|
||||||
|
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
||||||
|
hasErrors: false,
|
||||||
|
netPerformance: new Big('17.68'),
|
||||||
|
netPerformancePercentage: new Big('0.11662269129287598945'),
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
averagePrice: new Big('75.80'),
|
||||||
|
currency: 'CHF',
|
||||||
|
dataSource: 'YAHOO',
|
||||||
|
firstBuyDate: '2022-03-07',
|
||||||
|
grossPerformance: new Big('21.93'),
|
||||||
|
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
||||||
|
investment: new Big('75.80'),
|
||||||
|
netPerformance: new Big('17.68'),
|
||||||
|
netPerformancePercentage: new Big('0.11662269129287598945'),
|
||||||
|
marketPrice: 87.8,
|
||||||
|
quantity: new Big('1'),
|
||||||
|
symbol: 'NOVN.SW',
|
||||||
|
transactionCount: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalInvestment: new Big('75.80')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -77,17 +77,30 @@ export class PortfolioCalculator {
|
|||||||
const newQuantity = order.quantity
|
const newQuantity = order.quantity
|
||||||
.mul(factor)
|
.mul(factor)
|
||||||
.plus(oldAccumulatedSymbol.quantity);
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
|
|
||||||
|
let investment = new Big(0);
|
||||||
|
|
||||||
|
if (newQuantity.gt(0)) {
|
||||||
|
if (order.type === 'BUY') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.plus(
|
||||||
|
order.quantity.mul(unitPrice)
|
||||||
|
);
|
||||||
|
} else if (order.type === 'SELL') {
|
||||||
|
const averagePrice = oldAccumulatedSymbol.investment.div(
|
||||||
|
oldAccumulatedSymbol.quantity
|
||||||
|
);
|
||||||
|
investment = oldAccumulatedSymbol.investment.minus(
|
||||||
|
order.quantity.mul(averagePrice)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentTransactionPointItem = {
|
currentTransactionPointItem = {
|
||||||
|
investment,
|
||||||
currency: order.currency,
|
currency: order.currency,
|
||||||
dataSource: order.dataSource,
|
dataSource: order.dataSource,
|
||||||
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
investment: newQuantity.eq(0)
|
|
||||||
? new Big(0)
|
|
||||||
: unitPrice
|
|
||||||
.mul(order.quantity)
|
|
||||||
.mul(factor)
|
|
||||||
.plus(oldAccumulatedSymbol.investment),
|
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
|
@ -17,8 +17,6 @@ import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RakutenRapidApiService implements DataProviderInterface {
|
||||||
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
|
@ -16,7 +16,6 @@ import {
|
|||||||
DataSource,
|
DataSource,
|
||||||
SymbolProfile
|
SymbolProfile
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { countries } from 'countries-list';
|
import { countries } from 'countries-list';
|
||||||
import { addDays, format, isSameDay } from 'date-fns';
|
import { addDays, format, isSameDay } from 'date-fns';
|
||||||
@ -25,8 +24,6 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YahooFinanceService implements DataProviderInterface {
|
export class YahooFinanceService implements DataProviderInterface {
|
||||||
private readonly yahooFinanceHostname = 'https://query1.finance.yahoo.com';
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly cryptocurrencyService: CryptocurrencyService
|
private readonly cryptocurrencyService: CryptocurrencyService
|
||||||
) {}
|
) {}
|
||||||
@ -244,16 +241,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
const items: LookupItem[] = [];
|
const items: LookupItem[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const get = bent(
|
const searchResult = await yahooFinance.search(aQuery);
|
||||||
`${this.yahooFinanceHostname}/v1/finance/search?q=${encodeURIComponent(
|
|
||||||
aQuery
|
|
||||||
)}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
|
||||||
'GET',
|
|
||||||
'json',
|
|
||||||
200
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchResult = await get();
|
|
||||||
|
|
||||||
const quotes = searchResult.quotes
|
const quotes = searchResult.quotes
|
||||||
.filter((quote) => {
|
.filter((quote) => {
|
||||||
@ -279,20 +267,24 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const marketData = await this.getQuotes(
|
const marketData = await yahooFinance.quote(
|
||||||
quotes.map(({ symbol }) => {
|
quotes.map(({ symbol }) => {
|
||||||
return symbol;
|
return symbol;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [symbol, value] of Object.entries(marketData)) {
|
for (const marketDataItem of marketData) {
|
||||||
const quote = quotes.find((currentQuote: any) => {
|
const quote = quotes.find((currentQuote) => {
|
||||||
return currentQuote.symbol === symbol;
|
return currentQuote.symbol === marketDataItem.symbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const symbol = this.convertFromYahooFinanceSymbol(
|
||||||
|
marketDataItem.symbol
|
||||||
|
);
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
symbol,
|
symbol,
|
||||||
currency: value.currency,
|
currency: marketDataItem.currency,
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
name: quote?.longname || quote?.shortname || symbol
|
name: quote?.longname || quote?.shortname || symbol
|
||||||
});
|
});
|
||||||
|
@ -194,16 +194,17 @@
|
|||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
<button mat-menu-item (click)="onUpdateAccount(element)">
|
||||||
Edit
|
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||||
|
<span i18n>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="element.isDefault || element.Order?.length > 0"
|
[disabled]="element.isDefault || element.Order?.length > 0"
|
||||||
(click)="onDeleteAccount(element.id)"
|
(click)="onDeleteAccount(element.id)"
|
||||||
>
|
>
|
||||||
Delete
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -68,12 +68,12 @@
|
|||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
<button
|
<button
|
||||||
i18n
|
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="userItem.id === user?.id"
|
[disabled]="userItem.id === user?.id"
|
||||||
(click)="onDeleteUser(userItem.id)"
|
(click)="onDeleteUser(userItem.id)"
|
||||||
>
|
>
|
||||||
Delete
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -211,14 +211,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
)
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
downloadAsFile(
|
downloadAsFile({
|
||||||
data,
|
content: data,
|
||||||
`ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
|
fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
|
||||||
parseISO(data.meta.date),
|
parseISO(data.meta.date),
|
||||||
'yyyyMMddHHmm'
|
'yyyyMMddHHmm'
|
||||||
)}.json`,
|
)}.json`,
|
||||||
'text/plain'
|
format: 'json'
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
|
|||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -50,6 +51,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
|
private icsService: IcsService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private importTransactionsService: ImportTransactionsService,
|
private importTransactionsService: ImportTransactionsService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -152,14 +154,36 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.fetchExport(activityIds)
|
.fetchExport(activityIds)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
downloadAsFile(
|
for (const activity of data.activities) {
|
||||||
data,
|
delete activity.id;
|
||||||
`ghostfolio-export-${format(
|
}
|
||||||
|
|
||||||
|
downloadAsFile({
|
||||||
|
content: data,
|
||||||
|
fileName: `ghostfolio-export-${format(
|
||||||
parseISO(data.meta.date),
|
parseISO(data.meta.date),
|
||||||
'yyyyMMddHHmm'
|
'yyyyMMddHHmm'
|
||||||
)}.json`,
|
)}.json`,
|
||||||
'text/plain'
|
format: 'json'
|
||||||
);
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExportDrafts(activityIds?: string[]) {
|
||||||
|
this.dataService
|
||||||
|
.fetchExport(activityIds)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
downloadAsFile({
|
||||||
|
content: this.icsService.transformActivitiesToIcsContent(
|
||||||
|
data.activities
|
||||||
|
),
|
||||||
|
contentType: 'text/calendar',
|
||||||
|
fileName: `ghostfolio-draft${
|
||||||
|
data.activities.length > 1 ? 's' : ''
|
||||||
|
}-${format(parseISO(data.meta.date), 'yyyyMMddHHmm')}.ics`,
|
||||||
|
format: 'string'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
(activityToClone)="onCloneTransaction($event)"
|
(activityToClone)="onCloneTransaction($event)"
|
||||||
(activityToUpdate)="onUpdateTransaction($event)"
|
(activityToUpdate)="onUpdateTransaction($event)"
|
||||||
(export)="onExport($event)"
|
(export)="onExport($event)"
|
||||||
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
></gf-activities-table>
|
></gf-activities-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,105 +1,117 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Resources</h3>
|
<h1 class="d-flex h3 justify-content-center mb-3" i18n>Resources</h1>
|
||||||
<mat-card class="mb-3">
|
<h2 class="h4 mb-3">Guides</h2>
|
||||||
<mat-card-content>
|
<div class="mb-5">
|
||||||
<h4 class="mb-3">Market</h4>
|
<div class="mb-4 media">
|
||||||
<div class="mb-5">
|
<div class="media-body">
|
||||||
<div class="mb-4 media">
|
<h3 class="h5 mt-0">Boringly Getting Rich</h3>
|
||||||
<div class="media-body">
|
<div class="mb-1">
|
||||||
<h5 class="mt-0">Fear & Greed Index</h5>
|
The <i>Boringly Getting Rich</i> guide supports you to get started
|
||||||
<div class="mb-1">
|
with investing. It introduces a strategy utilizing a broadly
|
||||||
The fear and greed index was developed by <i>CNNMoney</i> to
|
diversified, low-cost portfolio excluding the risks of individual
|
||||||
measure the primary emotions (fear and greed) that influence
|
stocks.
|
||||||
how much investors are willing to pay for stocks.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://money.cnn.com/data/fear-and-greed/"
|
|
||||||
target="_blank"
|
|
||||||
>Fear & Greed Index →</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media">
|
<div>
|
||||||
<div class="media-body">
|
<a href="https://herget.me/investing-guide" target="_blank"
|
||||||
<h5 class="mt-0">Inflation Chart</h5>
|
>Boringly Getting Rich →</a
|
||||||
<div class="mb-1">
|
>
|
||||||
Inflation Chart helps you find the intrinsic value of stock
|
|
||||||
markets, stock prices, goods and services by adjusting them to
|
|
||||||
the amount of the money supply (M0, M1, M2) or price of other
|
|
||||||
goods (food or oil).
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="https://inflationchart.com" target="_blank"
|
|
||||||
>Inflation Chart →</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mb-3">Glossary</h4>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<div class="mb-4 media">
|
<h2 class="h4 mb-3">Market</h2>
|
||||||
<!--<img src="" class="mr-3" />-->
|
<div class="mb-5">
|
||||||
<div class="media-body">
|
<div class="mb-4 media">
|
||||||
<h5 class="mt-0">Buy and Hold</h5>
|
<div class="media-body">
|
||||||
<div class="mb-1">
|
<h3 class="h5 mt-0">Fear & Greed Index</h3>
|
||||||
Buy and hold is a passive investment strategy where you buy
|
<div class="mb-1">
|
||||||
assets and hold them for a long period regardless of
|
The fear and greed index was developed by <i>CNNMoney</i> to
|
||||||
fluctuations in the market.
|
measure the primary emotions (fear and greed) that influence how
|
||||||
</div>
|
much investors are willing to pay for stocks.
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://www.investopedia.com/terms/b/buyandhold.asp"
|
|
||||||
target="_blank"
|
|
||||||
>Buy and Hold →</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 media">
|
<div>
|
||||||
<!--<img src="" class="mr-3" />-->
|
<a
|
||||||
<div class="media-body">
|
href="https://money.cnn.com/data/fear-and-greed/"
|
||||||
<h5 class="mt-0">Dollar-Cost Averaging (DCA)</h5>
|
target="_blank"
|
||||||
<div class="mb-1">
|
>Fear & Greed Index →</a
|
||||||
Dollar-cost averaging is an investment strategy where you
|
>
|
||||||
split the total amount to be invested across periodic
|
|
||||||
purchases of a target asset to reduce the impact of volatility
|
|
||||||
on the overall purchase.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://www.investopedia.com/terms/d/dollarcostaveraging.asp"
|
|
||||||
target="_blank"
|
|
||||||
>Dollar-Cost Averaging →</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="media">
|
|
||||||
<!--<img src="" class="mr-3" />-->
|
|
||||||
<div class="media-body">
|
|
||||||
<h5 class="mt-0">Financial Independence</h5>
|
|
||||||
<div class="mb-1">
|
|
||||||
Financial independence is the status of having enough income,
|
|
||||||
for example with a passive income like dividends, to cover
|
|
||||||
your living expenses for the rest of your life.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://en.wikipedia.org/wiki/Financial_independence"
|
|
||||||
target="_blank"
|
|
||||||
>Financial Independence →</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</div>
|
||||||
</mat-card>
|
<div class="media">
|
||||||
|
<div class="media-body">
|
||||||
|
<h3 class="h5 mt-0">Inflation Chart</h3>
|
||||||
|
<div class="mb-1">
|
||||||
|
Inflation Chart helps you find the intrinsic value of stock
|
||||||
|
markets, stock prices, goods and services by adjusting them to the
|
||||||
|
amount of the money supply (M0, M1, M2) or price of other goods
|
||||||
|
(food or oil).
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://inflationchart.com" target="_blank"
|
||||||
|
>Inflation Chart →</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="h4 mb-3">Glossary</h2>
|
||||||
|
<div>
|
||||||
|
<div class="mb-4 media">
|
||||||
|
<div class="media-body">
|
||||||
|
<h3 class="h5 mt-0">Buy and Hold</h3>
|
||||||
|
<div class="mb-1">
|
||||||
|
Buy and hold is a passive investment strategy where you buy assets
|
||||||
|
and hold them for a long period regardless of fluctuations in the
|
||||||
|
market.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://www.investopedia.com/terms/b/buyandhold.asp"
|
||||||
|
target="_blank"
|
||||||
|
>Buy and Hold →</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 media">
|
||||||
|
<div class="media-body">
|
||||||
|
<h3 class="h5 mt-0">Dollar-Cost Averaging (DCA)</h3>
|
||||||
|
<div class="mb-1">
|
||||||
|
Dollar-cost averaging is an investment strategy where you split
|
||||||
|
the total amount to be invested across periodic purchases of a
|
||||||
|
target asset to reduce the impact of volatility on the overall
|
||||||
|
purchase.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://www.investopedia.com/terms/d/dollarcostaveraging.asp"
|
||||||
|
target="_blank"
|
||||||
|
>Dollar-Cost Averaging →</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-body">
|
||||||
|
<h3 class="h5 mt-0">Financial Independence</h3>
|
||||||
|
<div class="mb-1">
|
||||||
|
Financial independence is the status of having enough income, for
|
||||||
|
example with a passive income like dividends, to cover your living
|
||||||
|
expenses for the rest of your life.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://en.wikipedia.org/wiki/Financial_independence"
|
||||||
|
target="_blank"
|
||||||
|
>Financial Independence →</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,14 +2,12 @@
|
|||||||
color: rgb(var(--dark-primary-text));
|
color: rgb(var(--dark-primary-text));
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-card {
|
a {
|
||||||
a {
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
color: rgba(var(--palette-primary-500), 1);
|
font-weight: 500;
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(var(--palette-primary-300), 1);
|
color: rgba(var(--palette-primary-300), 1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
apps/client/src/app/services/ics/ics.service.ts
Normal file
60
apps/client/src/app/services/ics/ics.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { capitalize } from '@ghostfolio/common/helper';
|
||||||
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Type } from '@prisma/client';
|
||||||
|
import { format, parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class IcsService {
|
||||||
|
private readonly ICS_DATE_FORMAT = 'yyyyMMdd';
|
||||||
|
private readonly ICS_LINE_BREAK = '\r\n';
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public transformActivitiesToIcsContent(
|
||||||
|
aActivities: Export['activities']
|
||||||
|
): string {
|
||||||
|
const header = [
|
||||||
|
'BEGIN:VCALENDAR',
|
||||||
|
'VERSION:2.0',
|
||||||
|
'PRODID:-//Ghostfolio//NONSGML v1.0//EN'
|
||||||
|
];
|
||||||
|
const events = aActivities.map((activity) => {
|
||||||
|
return this.getEvent({
|
||||||
|
date: parseISO(activity.date),
|
||||||
|
id: activity.id,
|
||||||
|
symbol: activity.symbol,
|
||||||
|
type: activity.type
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const footer = ['END:VCALENDAR'];
|
||||||
|
|
||||||
|
return [...header, ...events, ...footer].join(this.ICS_LINE_BREAK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEvent({
|
||||||
|
date,
|
||||||
|
id,
|
||||||
|
symbol,
|
||||||
|
type
|
||||||
|
}: {
|
||||||
|
date: Date;
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
type: Type;
|
||||||
|
}) {
|
||||||
|
const today = format(new Date(), this.ICS_DATE_FORMAT);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'BEGIN:VEVENT',
|
||||||
|
`UID:${id}`,
|
||||||
|
`DTSTAMP:${today}T000000`,
|
||||||
|
`DTSTART;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`,
|
||||||
|
`DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`,
|
||||||
|
`SUMMARY:${capitalize(type)} ${symbol}`,
|
||||||
|
'END:VEVENT'
|
||||||
|
].join(this.ICS_LINE_BREAK);
|
||||||
|
}
|
||||||
|
}
|
@ -12,17 +12,28 @@ export function decodeDataSource(encodedDataSource: string) {
|
|||||||
return Buffer.from(encodedDataSource, 'hex').toString();
|
return Buffer.from(encodedDataSource, 'hex').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadAsFile(
|
export function downloadAsFile({
|
||||||
aContent: unknown,
|
content,
|
||||||
aFileName: string,
|
contentType = 'text/plain',
|
||||||
aContentType: string
|
fileName,
|
||||||
) {
|
format
|
||||||
|
}: {
|
||||||
|
content: unknown;
|
||||||
|
contentType?: string;
|
||||||
|
fileName: string;
|
||||||
|
format: 'json' | 'string';
|
||||||
|
}) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], {
|
|
||||||
type: aContentType
|
if (format === 'json') {
|
||||||
|
content = JSON.stringify(content, undefined, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = new Blob([<string>content], {
|
||||||
|
type: contentType
|
||||||
});
|
});
|
||||||
a.href = URL.createObjectURL(file);
|
a.href = URL.createObjectURL(file);
|
||||||
a.download = aFileName;
|
a.download = fileName;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,5 +5,14 @@ export interface Export {
|
|||||||
date: string;
|
date: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
activities: Partial<Order>[];
|
activities: (Omit<
|
||||||
|
Order,
|
||||||
|
| 'accountUserId'
|
||||||
|
| 'createdAt'
|
||||||
|
| 'date'
|
||||||
|
| 'isDraft'
|
||||||
|
| 'symbolProfileId'
|
||||||
|
| 'updatedAt'
|
||||||
|
| 'userId'
|
||||||
|
> & { date: string; symbol: string })[];
|
||||||
}
|
}
|
||||||
|
@ -271,6 +271,7 @@
|
|||||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
*ngIf="totalValue !== null"
|
||||||
[isAbsolute]="true"
|
[isAbsolute]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
@ -302,6 +303,7 @@
|
|||||||
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
*ngIf="totalValue !== null"
|
||||||
[isAbsolute]="true"
|
[isAbsolute]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
@ -356,11 +358,22 @@
|
|||||||
*ngIf="hasPermissionToExportActivities"
|
*ngIf="hasPermissionToExportActivities"
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
|
[disabled]="dataSource.data.length === 0"
|
||||||
(click)="onExport()"
|
(click)="onExport()"
|
||||||
>
|
>
|
||||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||||
<span i18n>Export</span>
|
<span i18n>Export</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="!hasDrafts"
|
||||||
|
(click)="onExportDrafts()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||||
|
<span i18n>Export Drafts as ICS</span>
|
||||||
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
@ -374,14 +387,25 @@
|
|||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #activityMenu="matMenu" xPosition="before">
|
<mat-menu #activityMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
|
<button mat-menu-item (click)="onUpdateActivity(element)">
|
||||||
Edit
|
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||||
|
<span i18n>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onCloneActivity(element)">
|
<button mat-menu-item (click)="onCloneActivity(element)">
|
||||||
Clone
|
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
|
||||||
|
<span i18n>Clone</span>
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
|
<button
|
||||||
Delete
|
mat-menu-item
|
||||||
|
[disabled]="!element.isDraft"
|
||||||
|
(click)="onExportDraft(element.id)"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||||
|
<span i18n>Export Draft as ICS</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="onDeleteActivity(element.id)">
|
||||||
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||||
|
<span i18n>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -56,6 +56,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() export = new EventEmitter<string[]>();
|
@Output() export = new EventEmitter<string[]>();
|
||||||
|
@Output() exportDrafts = new EventEmitter<string[]>();
|
||||||
@Output() import = new EventEmitter<void>();
|
@Output() import = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||||
@ -68,6 +69,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
public endOfToday = endOfToday();
|
public endOfToday = endOfToday();
|
||||||
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
||||||
public filters: Observable<string[]> = this.filters$.asObservable();
|
public filters: Observable<string[]> = this.filters$.asObservable();
|
||||||
|
public hasDrafts = false;
|
||||||
public isAfter = isAfter;
|
public isAfter = isAfter;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public isUUID = isUUID;
|
public isUUID = isUUID;
|
||||||
@ -198,6 +200,22 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onExportDraft(aActivityId: string) {
|
||||||
|
this.exportDrafts.emit([aActivityId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExportDrafts() {
|
||||||
|
this.exportDrafts.emit(
|
||||||
|
this.dataSource.filteredData
|
||||||
|
.filter((activity) => {
|
||||||
|
return activity.isDraft;
|
||||||
|
})
|
||||||
|
.map((activity) => {
|
||||||
|
return activity.id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public onImport() {
|
public onImport() {
|
||||||
this.import.emit();
|
this.import.emit();
|
||||||
}
|
}
|
||||||
@ -234,6 +252,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.filters$.next(this.allFilters);
|
this.filters$.next(this.allFilters);
|
||||||
|
|
||||||
|
this.hasDrafts = this.dataSource.data.some((activity) => {
|
||||||
|
return activity.isDraft === true;
|
||||||
|
});
|
||||||
this.totalFees = this.getTotalFees();
|
this.totalFees = this.getTotalFees();
|
||||||
this.totalValue = this.getTotalValue();
|
this.totalValue = this.getTotalValue();
|
||||||
}
|
}
|
||||||
@ -308,7 +329,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
if (activity.type === 'BUY' || activity.type === 'ITEM') {
|
||||||
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
totalValue = totalValue.plus(activity.valueInBaseCurrency);
|
||||||
} else if (activity.type === 'SELL') {
|
} else if (activity.type === 'SELL') {
|
||||||
totalValue = totalValue.minus(activity.valueInBaseCurrency);
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormControl } from '@angular/forms';
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||||
import { transformTickToAbbreviation } from '@ghostfolio/common/helper';
|
import { transformTickToAbbreviation } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
BarController,
|
BarController,
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
LinearScale,
|
LinearScale,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
|
import * as Color from 'color';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@ -211,16 +212,30 @@ export class FireCalculatorComponent
|
|||||||
labels.push(year);
|
labels.push(year);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const datasetDeposit = {
|
||||||
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
|
data: [],
|
||||||
|
label: 'Deposit'
|
||||||
|
};
|
||||||
|
|
||||||
const datasetInterest = {
|
const datasetInterest = {
|
||||||
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
backgroundColor: Color(
|
||||||
|
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
|
||||||
|
)
|
||||||
|
.lighten(0.5)
|
||||||
|
.hex(),
|
||||||
data: [],
|
data: [],
|
||||||
label: 'Interest'
|
label: 'Interest'
|
||||||
};
|
};
|
||||||
|
|
||||||
const datasetPrincipal = {
|
const datasetSavings = {
|
||||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
backgroundColor: Color(
|
||||||
|
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
|
||||||
|
)
|
||||||
|
.lighten(0.25)
|
||||||
|
.hex(),
|
||||||
data: [],
|
data: [],
|
||||||
label: 'Principal'
|
label: 'Savings'
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let period = 1; period <= t; period++) {
|
for (let period = 1; period <= t; period++) {
|
||||||
@ -232,8 +247,9 @@ export class FireCalculatorComponent
|
|||||||
r
|
r
|
||||||
});
|
});
|
||||||
|
|
||||||
datasetPrincipal.data.push(principal.toNumber());
|
datasetDeposit.data.push(this.fireWealth);
|
||||||
datasetInterest.data.push(interest.toNumber());
|
datasetInterest.data.push(interest.toNumber());
|
||||||
|
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
|
||||||
|
|
||||||
if (period === t) {
|
if (period === t) {
|
||||||
this.projectedTotalAmount = totalAmount.toNumber();
|
this.projectedTotalAmount = totalAmount.toNumber();
|
||||||
@ -242,7 +258,7 @@ export class FireCalculatorComponent
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
labels,
|
labels,
|
||||||
datasets: [datasetPrincipal, datasetInterest]
|
datasets: [datasetDeposit, datasetSavings, datasetInterest]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.136.0",
|
"version": "1.138.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Reference in New Issue
Block a user