Browse Source

Merge pull request #7 from dandevaud/feature/Some_performance_optimizations

Feature/some performance optimizations
pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
10578ee1c7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
  2. 75
      apps/api/src/app/portfolio/portfolio-calculator.ts
  3. 84
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 246
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 3
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  6. 16
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  7. 6
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  8. 8
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  9. 2
      apps/client/src/app/services/import-activities.service.ts
  10. 9
      libs/ui/src/lib/activities-table/activities-table.component.html
  11. 4
      libs/ui/src/lib/activities-table/activities-table.component.scss
  12. 1
      prisma/schema.prisma

1
apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts

@ -10,6 +10,7 @@ export interface PortfolioPositionDetail {
averagePrice: number; averagePrice: number;
dataProviderInfo: DataProviderInfo; dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
stakeRewards: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: number; grossPerformance: number;

75
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -3,6 +3,7 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
HistoricalDataItem,
ResponseError, ResponseError,
TimelinePosition TimelinePosition
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -92,7 +93,7 @@ export class PortfolioCalculator {
let investment = new Big(0); let investment = new Big(0);
if (newQuantity.gt(0)) { if (newQuantity.gt(0)) {
if (order.type === 'BUY') { if (order.type === 'BUY' || order.type === 'STAKE') {
investment = oldAccumulatedSymbol.investment.plus( investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice) order.quantity.mul(unitPrice)
); );
@ -277,46 +278,29 @@ export class PortfolioCalculator {
}; };
} }
for (const currentDate of dates) { return dates.map((date) => {
const dateString = format(currentDate, DATE_FORMAT); const dateString = format(date, DATE_FORMAT);
let totalCurrentValue = new Big(0);
let totalInvestmentValue = new Big(0);
let maxTotalInvestmentValue = new Big(0);
let totalNetPerformanceValue = new Big(0);
for (const symbol of Object.keys(valuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol]; const symbolValues = valuesBySymbol[symbol];
const currentValue = totalCurrentValue = totalCurrentValue.plus(
symbolValues.currentValues?.[dateString] ?? new Big(0); symbolValues.currentValues?.[dateString] ?? new Big(0)
const investmentValue = );
symbolValues.investmentValues?.[dateString] ?? new Big(0); totalInvestmentValue = totalInvestmentValue.plus(
const maxInvestmentValue = symbolValues.investmentValues?.[dateString] ?? new Big(0)
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0); );
const netPerformanceValue = maxTotalInvestmentValue = maxTotalInvestmentValue.plus(
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0)
);
valuesByDate[dateString] = { totalNetPerformanceValue = totalNetPerformanceValue.plus(
totalCurrentValue: ( symbolValues.netPerformanceValues?.[dateString] ?? new Big(0)
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0) );
).add(currentValue),
totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
).add(investmentValue),
maxTotalInvestmentValue: (
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
).add(maxInvestmentValue),
totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
).add(netPerformanceValue)
};
}
} }
return Object.entries(valuesByDate).map(([date, values]) => {
const {
maxTotalInvestmentValue,
totalCurrentValue,
totalInvestmentValue,
totalNetPerformanceValue
} = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0 ? 0
: totalNetPerformanceValue : totalNetPerformanceValue
@ -325,7 +309,7 @@ export class PortfolioCalculator {
.toNumber(); .toNumber();
return { return {
date, date: dateString,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValue.toNumber(), totalInvestment: totalInvestmentValue.toNumber(),
@ -931,6 +915,7 @@ export class PortfolioCalculator {
switch (type) { switch (type) {
case 'BUY': case 'BUY':
case 'STAKE':
factor = 1; factor = 1;
break; break;
case 'SELL': case 'SELL':
@ -1087,6 +1072,20 @@ export class PortfolioCalculator {
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice lastUnitPrice
}); });
} else {
let orderIndex = orders.findIndex(
(o) => o.date === format(day, DATE_FORMAT) && o.type === 'STAKE'
);
if (orderIndex >= 0) {
let order = orders[orderIndex];
orders.splice(orderIndex, 1);
orders.push({
...order,
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice
});
}
} }
lastUnitPrice = last(orders).unitPrice; lastUnitPrice = last(orders).unitPrice;
@ -1156,7 +1155,7 @@ export class PortfolioCalculator {
} }
const transactionInvestment = const transactionInvestment =
order.type === 'BUY' order.type === 'BUY' || order.type === 'STAKE'
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) ? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
: totalUnits.gt(0) : totalUnits.gt(0)
? totalInvestment ? totalInvestment

84
apps/api/src/app/portfolio/portfolio.controller.ts

@ -111,46 +111,21 @@ export class PortfolioController {
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) let investmentTuple: [number, number] = [0, 0];
.map((portfolioPosition) => { for (let holding of Object.entries(holdings)) {
return portfolioPosition.investment; var portfolioPosition = holding[1];
}) investmentTuple[0] += portfolioPosition.investment;
.reduce((a, b) => a + b, 0); investmentTuple[1] += this.exchangeRateDataService.toCurrency(
const totalValue = Object.values(holdings)
.map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency, portfolioPosition.currency,
this.request.user.Settings.settings.baseCurrency this.request.user.Settings.settings.baseCurrency
); );
})
.reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null;
portfolioPosition.investment =
portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage =
portfolioPosition.value / totalValue;
}
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
accounts[name].valueInPercentage = valueInBaseCurrency / totalValue;
} }
const totalInvestment = investmentTuple[0];
for (const [name, { valueInBaseCurrency }] of Object.entries(platforms)) { const totalValue = investmentTuple[1];
platforms[name].valueInPercentage = valueInBaseCurrency / totalValue;
}
}
if ( if (hasDetails === false) {
hasDetails === false ||
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
'cash', 'cash',
'committedFunds', 'committedFunds',
@ -170,15 +145,40 @@ export class PortfolioController {
} }
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = { portfolioPosition.grossPerformance = null;
...portfolioPosition, portfolioPosition.investment =
assetClass: hasDetails ? portfolioPosition.assetClass : undefined, portfolioPosition.investment / totalInvestment;
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined, portfolioPosition.netPerformance = null;
countries: hasDetails ? portfolioPosition.countries : [], portfolioPosition.quantity = null;
currency: hasDetails ? portfolioPosition.currency : undefined, portfolioPosition.valueInPercentage =
markets: hasDetails ? portfolioPosition.markets : undefined, portfolioPosition.value / totalValue;
sectors: hasDetails ? portfolioPosition.sectors : [] (portfolioPosition.assetClass = hasDetails
}; ? portfolioPosition.assetClass
: undefined),
(portfolioPosition.assetSubClass = hasDetails
? portfolioPosition.assetSubClass
: undefined),
(portfolioPosition.countries = hasDetails
? portfolioPosition.countries
: []),
(portfolioPosition.currency = hasDetails
? portfolioPosition.currency
: undefined),
(portfolioPosition.markets = hasDetails
? portfolioPosition.markets
: undefined),
(portfolioPosition.sectors = hasDetails
? portfolioPosition.sectors
: []);
}
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
accounts[name].valueInPercentage = valueInBaseCurrency / totalValue;
}
for (const [name, { valueInBaseCurrency }] of Object.entries(platforms)) {
platforms[name].valueInPercentage = valueInBaseCurrency / totalValue;
}
} }
return { return {

246
apps/api/src/app/portfolio/portfolio.service.ts

@ -524,11 +524,9 @@ export class PortfolioService {
} }
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position;
}
for (const item of currentPositions.positions) { for (const item of currentPositions.positions) {
portfolioItemsNow[item.symbol] = item;
if (item.quantity.lte(0)) { if (item.quantity.lte(0)) {
// Ignore positions without any quantity // Ignore positions without any quantity
continue; continue;
@ -544,21 +542,7 @@ export class PortfolioService {
otherMarkets: 0 otherMarkets: 0
}; };
for (const country of symbolProfile.countries) { this.calculateMarketsAllocation(symbolProfile, markets);
if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
markets.emergingMarkets = new Big(markets.emergingMarkets)
.plus(country.weight)
.toNumber();
} else {
markets.otherMarkets = new Big(markets.otherMarkets)
.plus(country.weight)
.toNumber();
}
}
holdings[item.symbol] = { holdings[item.symbol] = {
markets, markets,
@ -589,6 +573,68 @@ export class PortfolioService {
}; };
} }
await this.handleCashPosition(
filters,
isFilteredByAccount,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
holdings
);
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
filters,
orders,
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts
});
filteredValueInBaseCurrency = await this.handleEmergencyFunds(
filters,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
emergencyFund,
orders,
accounts,
holdings
);
const summary = await this.getSummary({
impersonationId,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
activities: orders
})
});
return {
accounts,
holdings,
platforms,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
: 0,
hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth
};
}
private async handleCashPosition(
filters: Filter[],
isFilteredByAccount: boolean,
cashDetails: CashDetails,
userCurrency: string,
filteredValueInBaseCurrency: Big,
holdings: { [symbol: string]: PortfolioPosition }
) {
const isFilteredByCash = filters?.some((filter) => { const isFilteredByCash = filters?.some((filter) => {
return filter.type === 'ASSET_CLASS' && filter.id === 'CASH'; return filter.type === 'ASSET_CLASS' && filter.id === 'CASH';
}); });
@ -604,16 +650,26 @@ export class PortfolioService {
holdings[symbol] = cashPositions[symbol]; holdings[symbol] = cashPositions[symbol];
} }
} }
}
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ private async handleEmergencyFunds(
filters, filters: Filter[],
orders, cashDetails: CashDetails,
portfolioItemsNow, userCurrency: string,
userCurrency, filteredValueInBaseCurrency: Big,
userId, emergencyFund: Big,
withExcludedAccounts orders: Activity[],
}); accounts: {
[id: string]: {
balance: number;
currency: string;
name: string;
valueInBaseCurrency: number;
valueInPercentage?: number;
};
},
holdings: { [symbol: string]: PortfolioPosition }
) {
if ( if (
filters?.length === 1 && filters?.length === 1 &&
filters[0].id === EMERGENCY_FUND_TAG_ID && filters[0].id === EMERGENCY_FUND_TAG_ID &&
@ -648,30 +704,32 @@ export class PortfolioService {
value: emergencyFundInCash value: emergencyFundInCash
}; };
} }
return filteredValueInBaseCurrency;
}
const summary = await this.getSummary({ private calculateMarketsAllocation(
impersonationId, symbolProfile: EnhancedSymbolProfile,
userCurrency, markets: {
userId, developedMarkets: number;
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, emergingMarkets: number;
emergencyFundPositionsValueInBaseCurrency: otherMarkets: number;
this.getEmergencyFundPositionsValueInBaseCurrency({ }
activities: orders ) {
}) for (const country of symbolProfile.countries) {
}); if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
return { .plus(country.weight)
accounts, .toNumber();
holdings, } else if (emergingMarkets.includes(country.code)) {
platforms, markets.emergingMarkets = new Big(markets.emergingMarkets)
summary, .plus(country.weight)
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), .toNumber();
filteredValueInPercentage: summary.netWorth } else {
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() markets.otherMarkets = new Big(markets.otherMarkets)
: 0, .plus(country.weight)
hasErrors: currentPositions.hasErrors, .toNumber();
totalValueInBaseCurrency: summary.netWorth }
}; }
} }
public async getPosition( public async getPosition(
@ -704,6 +762,7 @@ export class PortfolioService {
averagePrice: undefined, averagePrice: undefined,
dataProviderInfo: undefined, dataProviderInfo: undefined,
dividendInBaseCurrency: undefined, dividendInBaseCurrency: undefined,
stakeRewards: undefined,
feeInBaseCurrency: undefined, feeInBaseCurrency: undefined,
firstBuyDate: undefined, firstBuyDate: undefined,
grossPerformance: undefined, grossPerformance: undefined,
@ -732,7 +791,11 @@ export class PortfolioService {
.filter((order) => { .filter((order) => {
tags = tags.concat(order.tags); tags = tags.concat(order.tags);
return order.type === 'BUY' || order.type === 'SELL'; return (
order.type === 'BUY' ||
order.type === 'SELL' ||
order.type === 'STAKE'
);
}) })
.map((order) => ({ .map((order) => ({
currency: order.SymbolProfile.currency, currency: order.SymbolProfile.currency,
@ -788,6 +851,16 @@ export class PortfolioService {
}) })
); );
const stakeRewards = getSum(
orders
.filter(({ type }) => {
return type === 'STAKE';
})
.map(({ quantity }) => {
return new Big(quantity);
})
);
// Convert investment, gross and net performance to currency of user // Convert investment, gross and net performance to currency of user
const investment = this.exchangeRateDataService.toCurrency( const investment = this.exchangeRateDataService.toCurrency(
position.investment?.toNumber(), position.investment?.toNumber(),
@ -882,6 +955,7 @@ export class PortfolioService {
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
stakeRewards: stakeRewards.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(), fee.toNumber(),
SymbolProfile.currency, SymbolProfile.currency,
@ -945,6 +1019,7 @@ export class PortfolioService {
averagePrice: 0, averagePrice: 0,
dataProviderInfo: undefined, dataProviderInfo: undefined,
dividendInBaseCurrency: 0, dividendInBaseCurrency: 0,
stakeRewards: 0,
feeInBaseCurrency: 0, feeInBaseCurrency: 0,
firstBuyDate: undefined, firstBuyDate: undefined,
grossPerformance: undefined, grossPerformance: undefined,
@ -1405,7 +1480,7 @@ export class PortfolioService {
let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0); let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0);
for (const order of emergencyFundOrders) { for (const order of emergencyFundOrders) {
if (order.type === 'BUY') { if (order.type === 'BUY' || order.type === 'STAKE') {
valueInBaseCurrencyOfEmergencyFundPositions = valueInBaseCurrencyOfEmergencyFundPositions =
valueInBaseCurrencyOfEmergencyFundPositions.plus( valueInBaseCurrencyOfEmergencyFundPositions.plus(
order.valueInBaseCurrency order.valueInBaseCurrency
@ -1592,38 +1667,63 @@ export class PortfolioService {
userId userId
}); });
const activities = await this.orderService.getOrders({ const ordersRaw = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts: true withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
}); });
const activities: Activity[] = [];
const dividend = this.getDividend({ const excludedActivities: Activity[] = [];
activities, let dividend = 0;
let fees = 0;
let items = 0;
let liabilities = 0;
let totalBuy = 0;
let totalSell = 0;
for (let order of ordersRaw) {
if (order.Account?.isExcluded ?? false) {
excludedActivities.push(order);
} else {
activities.push(order);
fees += this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency userCurrency
}).toNumber(); );
let amount = this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.SymbolProfile.currency,
userCurrency
);
switch (order.type) {
case 'DIVIDEND':
dividend += amount;
break;
case 'ITEM':
items += amount;
break;
case 'SELL':
totalSell += amount;
break;
case 'BUY':
totalBuy += amount;
break;
case 'LIABILITY':
liabilities += amount;
}
}
}
const emergencyFund = new Big( const emergencyFund = new Big(
Math.max( Math.max(
emergencyFundPositionsValueInBaseCurrency, emergencyFundPositionsValueInBaseCurrency,
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
) )
); );
const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date;
const items = this.getItems(activities).toNumber();
const liabilities = this.getLiabilities(activities).toNumber();
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); const firstOrderDate = activities[0]?.date;
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency) const cash = new Big(balanceInBaseCurrency)
.minus(emergencyFund) .minus(emergencyFund)
@ -1716,7 +1816,7 @@ export class PortfolioService {
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts, withExcludedAccounts,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL', 'STAKE']
}); });
if (orders.length <= 0) { if (orders.length <= 0) {

3
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts

@ -40,6 +40,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
}; };
public dataProviderInfo: DataProviderInfo; public dataProviderInfo: DataProviderInfo;
public dividendInBaseCurrency: number; public dividendInBaseCurrency: number;
public stakeRewards: number;
public feeInBaseCurrency: number; public feeInBaseCurrency: number;
public firstBuyDate: string; public firstBuyDate: string;
public grossPerformance: number; public grossPerformance: number;
@ -84,6 +85,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
averagePrice, averagePrice,
dataProviderInfo, dataProviderInfo,
dividendInBaseCurrency, dividendInBaseCurrency,
stakeRewards,
feeInBaseCurrency, feeInBaseCurrency,
firstBuyDate, firstBuyDate,
grossPerformance, grossPerformance,
@ -107,6 +109,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.countries = {}; this.countries = {};
this.dataProviderInfo = dataProviderInfo; this.dataProviderInfo = dataProviderInfo;
this.dividendInBaseCurrency = dividendInBaseCurrency; this.dividendInBaseCurrency = dividendInBaseCurrency;
this.stakeRewards = stakeRewards;
this.feeInBaseCurrency = feeInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate; this.firstBuyDate = firstBuyDate;
this.grossPerformance = grossPerformance; this.grossPerformance = grossPerformance;

16
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -126,7 +126,10 @@
>Investment</gf-value >Investment</gf-value
> >
</div> </div>
<div class="col-6 mb-3"> <div
*ngIf="dividendInBaseCurrency > 0 || !stakeRewards"
class="col-6 mb-3"
>
<gf-value <gf-value
i18n i18n
size="medium" size="medium"
@ -137,6 +140,17 @@
>Dividend</gf-value >Dividend</gf-value
> >
</div> </div>
<div *ngIf="stakeRewards > 0" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[precision]="quantityPrecision"
[value]="stakeRewards"
>Stake Rewards
</gf-value>
</div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n i18n

6
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -223,6 +223,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['quantity'].value * this.activityForm.controls['quantity'].value *
this.activityForm.controls['unitPrice'].value + this.activityForm.controls['unitPrice'].value +
this.activityForm.controls['fee'].value ?? 0; this.activityForm.controls['fee'].value ?? 0;
} else if (this.activityForm.controls['type'].value === 'STAKE') {
this.total =
this.activityForm.controls['quantity'].value *
this.currentMarketPrice ?? 0;
} else { } else {
this.total = this.total =
this.activityForm.controls['quantity'].value * this.activityForm.controls['quantity'].value *
@ -237,7 +241,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
if (this.activityForm.controls['searchSymbol'].invalid) { if (this.activityForm.controls['searchSymbol'].invalid) {
this.data.activity.SymbolProfile = null; this.data.activity.SymbolProfile = null;
} else if ( } else if (
['BUY', 'DIVIDEND', 'SELL'].includes( ['BUY', 'DIVIDEND', 'SELL', 'STAKE'].includes(
this.activityForm.controls['type'].value this.activityForm.controls['type'].value
) )
) { ) {

8
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -25,6 +25,9 @@
<mat-option class="line-height-1" value="DIVIDEND"> <mat-option class="line-height-1" value="DIVIDEND">
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span> <span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
</mat-option> </mat-option>
<mat-option class="line-height-1" value="STAKE">
<span><b>{{ typesTranslationMap['STAKE'] }}</b></span>
</mat-option>
<mat-option class="line-height-1" value="LIABILITY"> <mat-option class="line-height-1" value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span> <span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
<br /> <br />
@ -132,7 +135,10 @@
<input formControlName="quantity" matInput type="number" /> <input formControlName="quantity" matInput type="number" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="align-items-start d-flex mb-3"> <div
*ngIf="activityForm.controls['type']?.value !== 'STAKE'"
class="align-items-start d-flex mb-3"
>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value"> ><ng-container [ngSwitch]="activityForm.controls['type']?.value">

2
apps/client/src/app/services/import-activities.service.ts

@ -346,6 +346,8 @@ export class ImportActivitiesService {
return Type.LIABILITY; return Type.LIABILITY;
case 'sell': case 'sell':
return Type.SELL; return Type.SELL;
case 'stake':
return Type.STAKE;
default: default:
break; break;
} }

9
libs/ui/src/lib/activities-table/activities-table.component.html

@ -163,11 +163,16 @@
dividend: element.type === 'DIVIDEND', dividend: element.type === 'DIVIDEND',
item: element.type === 'ITEM', item: element.type === 'ITEM',
liability: element.type === 'LIABILITY', liability: element.type === 'LIABILITY',
sell: element.type === 'SELL' sell: element.type === 'SELL',
stake: element.type === 'STAKE'
}" }"
> >
<ion-icon <ion-icon
*ngIf="element.type === 'BUY' || element.type === 'DIVIDEND'" *ngIf="
element.type === 'BUY' ||
element.type === 'DIVIDEND' ||
element.type === 'STAKE'
"
name="arrow-up-circle-outline" name="arrow-up-circle-outline"
></ion-icon> ></ion-icon>
<ion-icon <ion-icon

4
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -33,6 +33,10 @@
color: var(--blue); color: var(--blue);
} }
&.stake {
color: var(--blue);
}
&.item { &.item {
color: var(--purple); color: var(--purple);
} }

1
prisma/schema.prisma

@ -240,6 +240,7 @@ enum Type {
ITEM ITEM
LIABILITY LIABILITY
SELL SELL
STAKE
} }
enum ViewMode { enum ViewMode {

Loading…
Cancel
Save