Merge branch 'main' of gitea.suda.codes:giteauser/ghostfolio-mirror
This commit is contained in:
commit
a597a58dad
@ -5,6 +5,12 @@ 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).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handled an exception in the historical market data gathering of derived currencies
|
||||
|
||||
## 2.112.0 - 2024-10-03
|
||||
|
||||
### Added
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AccessService } from '@ghostfolio/api/app/access/access.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||
@ -61,7 +60,6 @@ import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
|
||||
@Controller('portfolio')
|
||||
export class PortfolioController {
|
||||
public constructor(
|
||||
private readonly accessService: AccessService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
|
@ -45,6 +45,7 @@ import type {
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
GroupBy,
|
||||
Market,
|
||||
RequestWithUser,
|
||||
UserWithSettings
|
||||
} from '@ghostfolio/common/types';
|
||||
@ -581,6 +582,17 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
let markets: {
|
||||
[key in Market]: {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
|
||||
if (withMarkets) {
|
||||
markets = this.getAggregatedMarkets(holdings);
|
||||
}
|
||||
|
||||
let summary: PortfolioSummary;
|
||||
|
||||
if (withSummary) {
|
||||
@ -602,6 +614,7 @@ export class PortfolioService {
|
||||
accounts,
|
||||
hasErrors,
|
||||
holdings,
|
||||
markets,
|
||||
platforms,
|
||||
summary
|
||||
};
|
||||
@ -1148,74 +1161,49 @@ export class PortfolioService {
|
||||
|
||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
|
||||
const { activities } =
|
||||
await this.orderService.getOrdersForPortfolioCalculator({
|
||||
userCurrency,
|
||||
userId
|
||||
});
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
activities,
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: this.request.user.Settings.settings.baseCurrency
|
||||
});
|
||||
|
||||
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
|
||||
await portfolioCalculator.getSnapshot();
|
||||
|
||||
positions = positions.filter((item) => !item.quantity.eq(0));
|
||||
|
||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||
|
||||
for (const position of positions) {
|
||||
portfolioItemsNow[position.symbol] = position;
|
||||
}
|
||||
|
||||
const { accounts } = await this.getValueOfAccountsAndPlatforms({
|
||||
activities,
|
||||
portfolioItemsNow,
|
||||
userCurrency,
|
||||
userId
|
||||
});
|
||||
|
||||
const userSettings = <UserSettings>this.request.user.Settings.settings;
|
||||
|
||||
const { accounts, holdings, summary } = await this.getDetails({
|
||||
impersonationId,
|
||||
userId,
|
||||
withMarkets: true,
|
||||
withSummary: true
|
||||
});
|
||||
|
||||
return {
|
||||
rules: {
|
||||
accountClusterRisk: isEmpty(activities)
|
||||
? undefined
|
||||
: await this.rulesService.evaluate(
|
||||
[
|
||||
new AccountClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
accounts
|
||||
),
|
||||
new AccountClusterRiskSingleAccount(
|
||||
this.exchangeRateDataService,
|
||||
accounts
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
),
|
||||
currencyClusterRisk: isEmpty(activities)
|
||||
? undefined
|
||||
: await this.rulesService.evaluate(
|
||||
[
|
||||
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
positions
|
||||
),
|
||||
new CurrencyClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
positions
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
),
|
||||
accountClusterRisk:
|
||||
summary.ordersCount > 0
|
||||
? await this.rulesService.evaluate(
|
||||
[
|
||||
new AccountClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
accounts
|
||||
),
|
||||
new AccountClusterRiskSingleAccount(
|
||||
this.exchangeRateDataService,
|
||||
accounts
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
)
|
||||
: undefined,
|
||||
currencyClusterRisk:
|
||||
summary.ordersCount > 0
|
||||
? await this.rulesService.evaluate(
|
||||
[
|
||||
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
Object.values(holdings)
|
||||
),
|
||||
new CurrencyClusterRiskCurrentInvestment(
|
||||
this.exchangeRateDataService,
|
||||
Object.values(holdings)
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
)
|
||||
: undefined,
|
||||
emergencyFund: await this.rulesService.evaluate(
|
||||
[
|
||||
new EmergencyFundSetup(
|
||||
@ -1229,8 +1217,8 @@ export class PortfolioService {
|
||||
[
|
||||
new FeeRatioInitialInvestment(
|
||||
this.exchangeRateDataService,
|
||||
totalInvestment.toNumber(),
|
||||
totalFeesWithCurrencyEffect.toNumber()
|
||||
summary.committedFunds,
|
||||
summary.fees
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
@ -1257,6 +1245,62 @@ export class PortfolioService {
|
||||
await this.orderService.assignTags({ dataSource, symbol, tags, userId });
|
||||
}
|
||||
|
||||
private getAggregatedMarkets(holdings: {
|
||||
[symbol: string]: PortfolioPosition;
|
||||
}): {
|
||||
[key in Market]: { name: string; value: number };
|
||||
} {
|
||||
const markets = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
value: 0
|
||||
},
|
||||
developedMarkets: {
|
||||
name: 'developedMarkets',
|
||||
value: 0
|
||||
},
|
||||
emergingMarkets: {
|
||||
name: 'emergingMarkets',
|
||||
value: 0
|
||||
},
|
||||
otherMarkets: {
|
||||
name: 'otherMarkets',
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
|
||||
for (const [symbol, position] of Object.entries(holdings)) {
|
||||
const value = position.valueInBaseCurrency;
|
||||
|
||||
if (position.assetClass !== AssetClass.LIQUIDITY) {
|
||||
if (position.countries.length > 0) {
|
||||
markets.developedMarkets.value +=
|
||||
position.markets.developedMarkets * value;
|
||||
markets.emergingMarkets.value +=
|
||||
position.markets.emergingMarkets * value;
|
||||
markets.otherMarkets.value += position.markets.otherMarkets * value;
|
||||
} else {
|
||||
markets[UNKNOWN_KEY].value += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const marketsTotal =
|
||||
markets.developedMarkets.value +
|
||||
markets.emergingMarkets.value +
|
||||
markets.otherMarkets.value +
|
||||
markets[UNKNOWN_KEY].value;
|
||||
|
||||
markets.developedMarkets.value =
|
||||
markets.developedMarkets.value / marketsTotal;
|
||||
markets.emergingMarkets.value =
|
||||
markets.emergingMarkets.value / marketsTotal;
|
||||
markets.otherMarkets.value = markets.otherMarkets.value / marketsTotal;
|
||||
markets[UNKNOWN_KEY].value = markets[UNKNOWN_KEY].value / marketsTotal;
|
||||
|
||||
return markets;
|
||||
}
|
||||
|
||||
private async getCashPositions({
|
||||
cashDetails,
|
||||
userCurrency,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { groupBy } from '@ghostfolio/common/helper';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Big } from 'big.js';
|
||||
|
||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||
import { RuleInterface } from './interfaces/rule.interface';
|
||||
@ -33,24 +34,26 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public groupCurrentPositionsByAttribute(
|
||||
positions: TimelinePosition[],
|
||||
attribute: keyof TimelinePosition,
|
||||
public groupCurrentHoldingsByAttribute(
|
||||
holdings: PortfolioPosition[],
|
||||
attribute: keyof PortfolioPosition,
|
||||
baseCurrency: string
|
||||
) {
|
||||
return Array.from(groupBy(attribute, positions).entries()).map(
|
||||
return Array.from(groupBy(attribute, holdings).entries()).map(
|
||||
([attributeValue, objs]) => ({
|
||||
groupKey: attributeValue,
|
||||
investment: objs.reduce(
|
||||
(previousValue, currentValue) =>
|
||||
previousValue + currentValue.investment.toNumber(),
|
||||
previousValue + currentValue.investment,
|
||||
0
|
||||
),
|
||||
value: objs.reduce(
|
||||
(previousValue, currentValue) =>
|
||||
previousValue +
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
currentValue.quantity.mul(currentValue.marketPrice).toNumber(),
|
||||
new Big(currentValue.quantity)
|
||||
.mul(currentValue.marketPrice)
|
||||
.toNumber(),
|
||||
currentValue.currency,
|
||||
baseCurrency
|
||||
),
|
||||
|
@ -1,35 +1,34 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
||||
private positions: TimelinePosition[];
|
||||
private holdings: PortfolioPosition[];
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
positions: TimelinePosition[]
|
||||
holdings: PortfolioPosition[]
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name,
|
||||
name: 'Investment: Base Currency'
|
||||
});
|
||||
|
||||
this.positions = positions;
|
||||
this.holdings = holdings;
|
||||
}
|
||||
|
||||
public evaluate(ruleSettings: Settings) {
|
||||
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
|
||||
this.positions,
|
||||
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
|
||||
this.holdings,
|
||||
'currency',
|
||||
ruleSettings.baseCurrency
|
||||
);
|
||||
|
||||
let maxItem = positionsGroupedByCurrency[0];
|
||||
let maxItem = holdingsGroupedByCurrency[0];
|
||||
let totalValue = 0;
|
||||
|
||||
positionsGroupedByCurrency.forEach((groupItem) => {
|
||||
holdingsGroupedByCurrency.forEach((groupItem) => {
|
||||
// Calculate total value
|
||||
totalValue += groupItem.value;
|
||||
|
||||
@ -39,7 +38,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
|
||||
}
|
||||
});
|
||||
|
||||
const baseCurrencyItem = positionsGroupedByCurrency.find((item) => {
|
||||
const baseCurrencyItem = holdingsGroupedByCurrency.find((item) => {
|
||||
return item.groupKey === ruleSettings.baseCurrency;
|
||||
});
|
||||
|
||||
|
@ -1,35 +1,34 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import { TimelinePosition } from '@ghostfolio/common/models';
|
||||
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
private positions: TimelinePosition[];
|
||||
private holdings: PortfolioPosition[];
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
positions: TimelinePosition[]
|
||||
holdings: PortfolioPosition[]
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
key: CurrencyClusterRiskCurrentInvestment.name,
|
||||
name: 'Investment'
|
||||
});
|
||||
|
||||
this.positions = positions;
|
||||
this.holdings = holdings;
|
||||
}
|
||||
|
||||
public evaluate(ruleSettings: Settings) {
|
||||
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
|
||||
this.positions,
|
||||
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
|
||||
this.holdings,
|
||||
'currency',
|
||||
ruleSettings.baseCurrency
|
||||
);
|
||||
|
||||
let maxItem = positionsGroupedByCurrency[0];
|
||||
let maxItem = holdingsGroupedByCurrency[0];
|
||||
let totalValue = 0;
|
||||
|
||||
positionsGroupedByCurrency.forEach((groupItem) => {
|
||||
holdingsGroupedByCurrency.forEach((groupItem) => {
|
||||
// Calculate total value
|
||||
totalValue += groupItem.value;
|
||||
|
||||
|
@ -666,9 +666,13 @@ export class DataProviderService {
|
||||
} = {};
|
||||
|
||||
for (const date in rootData) {
|
||||
data[date] = {
|
||||
marketPrice: new Big(factor).mul(rootData[date].marketPrice).toNumber()
|
||||
};
|
||||
if (isNumber(rootData[date].marketPrice)) {
|
||||
data[date] = {
|
||||
marketPrice: new Big(factor)
|
||||
.mul(rootData[date].marketPrice)
|
||||
.toNumber()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -35,8 +35,6 @@ export class GfRuleSettingsDialogComponent {
|
||||
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams,
|
||||
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
|
||||
) {
|
||||
console.log(this.data.rule);
|
||||
|
||||
this.settings = this.data.rule.settings;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
PortfolioPosition,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Market } from '@ghostfolio/common/types';
|
||||
|
||||
export interface PortfolioDetails {
|
||||
accounts: {
|
||||
@ -14,6 +15,12 @@ export interface PortfolioDetails {
|
||||
};
|
||||
};
|
||||
holdings: { [symbol: string]: PortfolioPosition };
|
||||
markets?: {
|
||||
[key in Market]: {
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
platforms: {
|
||||
[id: string]: {
|
||||
balance: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user