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