mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Extend Public API with portfolio performance metrics endpoint * Update changelogpull/3792/head
Thomas Kaul
4 months ago
committed by
GitHub
14 changed files with 275 additions and 111 deletions
@ -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