Feature/extend public api with portfolio performance metrics endpoint (#3762)
* Extend Public API with portfolio performance metrics endpoint * Update changelogpull/3792/head
parent
9059d4f971
commit
583c14128b
@ -0,0 +1,134 @@
|
||||
import { AccessService } from '@ghostfolio/api/app/access/access.service';
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import { getSum } from '@ghostfolio/common/helper';
|
||||
import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { Big } from 'big.js';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
@Controller('public')
|
||||
export class PublicController {
|
||||
public constructor(
|
||||
private readonly accessService: AccessService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
@Get(':accessId/portfolio')
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getPublicPortfolio(
|
||||
@Param('accessId') accessId
|
||||
): Promise<PublicPortfolioResponse> {
|
||||
const access = await this.accessService.access({ id: accessId });
|
||||
|
||||
if (!access) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
let hasDetails = true;
|
||||
|
||||
const user = await this.userService.user({
|
||||
id: access.userId
|
||||
});
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
hasDetails = user.subscription.type === 'Premium';
|
||||
}
|
||||
|
||||
const [
|
||||
{ holdings },
|
||||
{ performance: performance1d },
|
||||
{ performance: performanceMax },
|
||||
{ performance: performanceYtd }
|
||||
] = await Promise.all([
|
||||
this.portfolioService.getDetails({
|
||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||
impersonationId: access.userId,
|
||||
userId: user.id,
|
||||
withMarkets: true
|
||||
}),
|
||||
...['1d', 'max', 'ytd'].map((dateRange) => {
|
||||
return this.portfolioService.getPerformance({
|
||||
dateRange,
|
||||
impersonationId: undefined,
|
||||
userId: user.id
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
const publicPortfolioResponse: PublicPortfolioResponse = {
|
||||
hasDetails,
|
||||
alias: access.alias,
|
||||
holdings: {},
|
||||
performance: {
|
||||
'1d': {
|
||||
relativeChange:
|
||||
performance1d.netPerformancePercentageWithCurrencyEffect
|
||||
},
|
||||
max: {
|
||||
relativeChange:
|
||||
performanceMax.netPerformancePercentageWithCurrencyEffect
|
||||
},
|
||||
ytd: {
|
||||
relativeChange:
|
||||
performanceYtd.netPerformancePercentageWithCurrencyEffect
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const totalValue = getSum(
|
||||
Object.values(holdings).map(({ currency, marketPrice, quantity }) => {
|
||||
return new Big(
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
quantity * marketPrice,
|
||||
currency,
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
DEFAULT_CURRENCY
|
||||
)
|
||||
);
|
||||
})
|
||||
).toNumber();
|
||||
|
||||
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
|
||||
publicPortfolioResponse.holdings[symbol] = {
|
||||
allocationInPercentage:
|
||||
portfolioPosition.valueInBaseCurrency / totalValue,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
dataSource: portfolioPosition.dataSource,
|
||||
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
name: portfolioPosition.name,
|
||||
netPerformancePercentWithCurrencyEffect:
|
||||
portfolioPosition.netPerformancePercentWithCurrencyEffect,
|
||||
sectors: hasDetails ? portfolioPosition.sectors : [],
|
||||
symbol: portfolioPosition.symbol,
|
||||
url: portfolioPosition.url,
|
||||
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue
|
||||
};
|
||||
}
|
||||
|
||||
return publicPortfolioResponse;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PublicController } from './public.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [PublicController],
|
||||
imports: [
|
||||
AccessModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
ImpersonationModule,
|
||||
MarketDataModule,
|
||||
OrderModule,
|
||||
PortfolioSnapshotQueueModule,
|
||||
PrismaModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule,
|
||||
TransformDataSourceInRequestModule,
|
||||
UserModule
|
||||
],
|
||||
providers: [
|
||||
AccountBalanceService,
|
||||
AccountService,
|
||||
CurrentRateService,
|
||||
PortfolioCalculatorFactory,
|
||||
PortfolioService,
|
||||
RulesService
|
||||
]
|
||||
})
|
||||
export class PublicModule {}
|
Loading…
Reference in new issue