diff --git a/apps/api/src/app/core/portfolio-calculator.spec.ts b/apps/api/src/app/core/portfolio-calculator.spec.ts index f9f40aa40..b840305a5 100644 --- a/apps/api/src/app/core/portfolio-calculator.spec.ts +++ b/apps/api/src/app/core/portfolio-calculator.spec.ts @@ -10,6 +10,7 @@ import { TimelineSpecification } from '@ghostfolio/api/app/core/portfolio-calculator'; import { OrderType } from '@ghostfolio/api/models/order-type'; +import { resetHours } from '@ghostfolio/common/helper'; import { Currency } from '@prisma/client'; import Big from 'big.js'; import { @@ -19,7 +20,6 @@ import { isBefore, parse } from 'date-fns'; -import { resetHours } from '@ghostfolio/common/helper'; function toYearMonthDay(date: Date) { const year = date.getFullYear(); @@ -583,7 +583,12 @@ describe('PortfolioCalculator', () => { marketPrice: 213.32, transactionCount: 5, grossPerformance: new Big('872.05'), // 213.32*25-4460.95 - grossPerformancePercentage: new Big('0.19548526659119694236') // 872.05/4460.95 + grossPerformancePercentage: new Big('0.19548526659119694236'), // 872.05/4460.95 + marketState: 'open', + name: '', + type: 'UNKNOWN', + url: '', + currency: 'USD' } }); }); diff --git a/apps/api/src/app/core/portfolio-calculator.ts b/apps/api/src/app/core/portfolio-calculator.ts index fb935ddc1..11d9297b7 100644 --- a/apps/api/src/app/core/portfolio-calculator.ts +++ b/apps/api/src/app/core/portfolio-calculator.ts @@ -3,6 +3,12 @@ import { GetValueObject } from '@ghostfolio/api/app/core/current-rate.service'; import { OrderType } from '@ghostfolio/api/models/order-type'; +import { + MarketState, + Type +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { resetHours } from '@ghostfolio/common/helper'; +import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { Currency } from '@prisma/client'; import Big from 'big.js'; import { @@ -14,7 +20,6 @@ import { isBefore, parse } from 'date-fns'; -import { resetHours } from '@ghostfolio/common/helper'; const DATE_FORMAT = 'yyyy-MM-dd'; @@ -130,14 +135,19 @@ export class PortfolioCalculator { .minus(item.investment); result[item.symbol] = { averagePrice: item.investment.div(item.quantity), + currency: item.currency, firstBuyDate: item.firstBuyDate, + marketState: MarketState.open, // TODO quantity: item.quantity, symbol: item.symbol, investment: item.investment, marketPrice: marketValue.marketPrice, transactionCount: item.transactionCount, grossPerformance, - grossPerformancePercentage: grossPerformance.div(item.investment) + grossPerformancePercentage: grossPerformance.div(item.investment), + url: '', // TODO + name: '', // TODO, + type: Type.Unknown // TODO }; } @@ -320,18 +330,6 @@ interface TransactionPointSymbol { transactionCount: number; } -interface TimelinePosition { - averagePrice: Big; - firstBuyDate: string; - quantity: Big; - symbol: string; - investment: Big; - grossPerformancePercentage: Big; - grossPerformance: Big; - marketPrice: number; - transactionCount: number; -} - type Accuracy = 'year' | 'month' | 'day'; export interface TimelineSpecification { diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-positions.interface.ts new file mode 100644 index 000000000..238f1718c --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/portfolio-positions.interface.ts @@ -0,0 +1,5 @@ +import { TimelinePosition } from '@ghostfolio/common/interfaces'; + +export interface PortfolioPositions { + positions: TimelinePosition[]; +} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index c70befe45..fef612fa5 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -37,6 +37,7 @@ import { HistoricalDataItem, PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; +import { PortfolioPositions } from './interfaces/portfolio-positions.interface'; import { PortfolioService } from './portfolio.service'; @Controller('portfolio') @@ -279,6 +280,16 @@ export class PortfolioController { return res.json(performance); } + @Get('positions') + @UseGuards(AuthGuard('jwt')) + public async getPositions( + @Headers('impersonation-id') impersonationId + ): Promise { + const positions = await this.portfolioService.getPositions(impersonationId); + + return { positions }; + } + @Get('position/:symbol') @UseGuards(AuthGuard('jwt')) public async getPosition( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index e6d87db1c..348a6f200 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -8,6 +8,7 @@ import { import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { OrderType } from '@ghostfolio/api/models/order-type'; import { Portfolio } from '@ghostfolio/api/models/portfolio'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; @@ -49,7 +50,6 @@ import { HistoricalDataItem, PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; -import { OrderType } from '@ghostfolio/api/models/order-type'; @Injectable() export class PortfolioService { @@ -151,30 +151,15 @@ export class PortfolioService { ); console.timeEnd('impersonation-service'); - console.time('create-portfolio'); const userId = impersonationUserId || this.request.user.id; - const orders = await this.getOrders(userId); - console.timeEnd('create-portfolio'); - - if (orders.length <= 0) { - return []; - } const portfolioCalculator = new PortfolioCalculator( this.currentRateService, this.request.user.Settings.currency ); - const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ - date: format(order.date, 'yyyy-MM-dd'), - quantity: new Big(order.quantity), - symbol: order.symbol, - type: order.type, - unitPrice: new Big(order.unitPrice), - currency: order.currency - })); - portfolioCalculator.computeTransactionPoints(portfolioOrders); - const transactionPoints = portfolioCalculator.getTransactionPoints(); + const transactionPoints = await this.getTransactionPoints(userId); + portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { return []; } @@ -211,6 +196,35 @@ export class PortfolioService { })); } + public async getPositions(aImpersonationId: string) { + const impersonationUserId = + await this.impersonationService.validateImpersonationId( + aImpersonationId, + this.request.user.id + ); + + const userId = impersonationUserId || this.request.user.id; + + const portfolioCalculator = new PortfolioCalculator( + this.currentRateService, + this.request.user.Settings.currency + ); + + const transactionPoints = await this.getTransactionPoints(userId); + + portfolioCalculator.setTransactionPoints(transactionPoints); + + const positions = await portfolioCalculator.getCurrentPositions(); + + return Object.values(positions).map((position) => { + return { + ...position, + grossPerformance: Number(position.grossPerformance), + grossPerformancePercentage: Number(position.grossPerformancePercentage) + }; + }); + } + private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': @@ -229,6 +243,32 @@ export class PortfolioService { return portfolioStart; } + private async getTransactionPoints(userId: string) { + console.time('create-portfolio'); + const orders = await this.getOrders(userId); + console.timeEnd('create-portfolio'); + + if (orders.length <= 0) { + return []; + } + + const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ + date: format(order.date, 'yyyy-MM-dd'), + quantity: new Big(order.quantity), + symbol: order.symbol, + type: order.type, + unitPrice: new Big(order.unitPrice), + currency: order.currency + })); + + const portfolioCalculator = new PortfolioCalculator( + this.currentRateService, + this.request.user.Settings.currency + ); + portfolioCalculator.computeTransactionPoints(portfolioOrders); + return portfolioCalculator.getTransactionPoints(); + } + public async getOverview( aImpersonationId: string ): Promise { diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 402778482..5556e6c7d 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -11,7 +11,7 @@ [isLoading]="isLoading" [marketState]="position?.marketState" [range]="range" - [value]="position?.grossPerformancePercent" + [value]="position?.grossPerformancePercentage" >
@@ -53,7 +53,7 @@ [colorizeSign]="true" [isPercent]="true" [locale]="locale" - [value]="position?.grossPerformancePercent" + [value]="position?.grossPerformancePercentage" >
diff --git a/apps/client/src/app/components/position/position.component.ts b/apps/client/src/app/components/position/position.component.ts index 4eba2ccac..840a1240a 100644 --- a/apps/client/src/app/components/position/position.component.ts +++ b/apps/client/src/app/components/position/position.component.ts @@ -8,7 +8,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { TimelinePosition } from '@ghostfolio/common/interfaces'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -25,7 +25,7 @@ export class PositionComponent implements OnDestroy, OnInit { @Input() deviceType: string; @Input() isLoading: boolean; @Input() locale: string; - @Input() position: PortfolioPosition; + @Input() position: TimelinePosition; @Input() range: string; public unknownKey = UNKNOWN_KEY; diff --git a/apps/client/src/app/components/positions/positions.component.ts b/apps/client/src/app/components/positions/positions.component.ts index cb308aef5..0c57f9bf7 100644 --- a/apps/client/src/app/components/positions/positions.component.ts +++ b/apps/client/src/app/components/positions/positions.component.ts @@ -9,7 +9,7 @@ import { MarketState, Type } from '@ghostfolio/api/services/interfaces/interfaces'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces/portfolio-position.interface'; +import { TimelinePosition } from '@ghostfolio/common/interfaces'; @Component({ selector: 'gf-positions', @@ -21,12 +21,12 @@ export class PositionsComponent implements OnChanges, OnInit { @Input() baseCurrency: string; @Input() deviceType: string; @Input() locale: string; - @Input() positions: { [symbol: string]: PortfolioPosition }; + @Input() positions: TimelinePosition[]; @Input() range: string; public hasPositions: boolean; - public positionsRest: PortfolioPosition[] = []; - public positionsWithPriority: PortfolioPosition[] = []; + public positionsRest: TimelinePosition[] = []; + public positionsWithPriority: TimelinePosition[] = []; private ignoreTypes = [Type.Cash]; @@ -36,7 +36,7 @@ export class PositionsComponent implements OnChanges, OnInit { public ngOnChanges() { if (this.positions) { - this.hasPositions = Object.entries(this.positions).length > 0; + this.hasPositions = this.positions.length > 0; if (!this.hasPositions) { return; @@ -45,7 +45,7 @@ export class PositionsComponent implements OnChanges, OnInit { this.positionsRest = []; this.positionsWithPriority = []; - for (const [, portfolioPosition] of Object.entries(this.positions)) { + for (const portfolioPosition of this.positions) { if (this.ignoreTypes.includes(portfolioPosition.type)) { continue; } diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 6d26e3d9c..169723dbf 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -24,7 +24,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioOverview, PortfolioPerformance, - PortfolioPosition, + TimelinePosition, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -65,7 +65,7 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit { public isLoadingPerformance = true; public overview: PortfolioOverview; public performance: PortfolioPerformance; - public positions: { [symbol: string]: PortfolioPosition }; + public positions: TimelinePosition[]; public routeQueryParams: Subscription; public user: User; @@ -231,10 +231,12 @@ export class HomePageComponent implements AfterViewInit, OnDestroy, OnInit { }); this.dataService - .fetchPortfolioPositions({ range: this.dateRange }) + .fetchPositions(/* { range: this.dateRange } */) // TODO .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { - this.positions = response; + console.log(response); + + this.positions = response.positions; this.hasPositions = this.positions && Object.keys(this.positions).length > 1; diff --git a/apps/client/src/app/pages/transactions/transactions-page.component.ts b/apps/client/src/app/pages/transactions/transactions-page.component.ts index 39eefdd32..15231a402 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -107,6 +107,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); this.fetchOrders(); + this.fetchPositions(); } public fetchOrders() { @@ -124,6 +125,15 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } + public fetchPositions() { + this.dataService + .fetchPositions() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + console.log(response); + }); + } + public onCloneTransaction(aTransaction: OrderModel) { this.openCreateTransactionDialog(aTransaction); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 0f52bb35e..e4b9194c9 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -9,6 +9,7 @@ import { HistoricalDataItem, PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; +import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; @@ -109,6 +110,14 @@ export class DataService { return this.http.get(`/api/symbol/${aSymbol}`); } + public fetchPositions(): Observable { + return this.http.get('/api/portfolio/positions').pipe( + map((respose) => { + return respose; + }) + ); + } + public fetchSymbols(aQuery: string) { return this.http .get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`) diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 35543c31e..d087eae7b 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -9,6 +9,7 @@ import { PortfolioPosition } from './portfolio-position.interface'; import { PortfolioReportRule } from './portfolio-report-rule.interface'; import { PortfolioReport } from './portfolio-report.interface'; import { Position } from './position.interface'; +import { TimelinePosition } from './timeline-position.interface'; import { UserSettings } from './user-settings.interface'; import { UserWithSettings } from './user-with-settings'; import { User } from './user.interface'; @@ -25,6 +26,7 @@ export { PortfolioReport, PortfolioReportRule, Position, + TimelinePosition, User, UserSettings, UserWithSettings diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts new file mode 100644 index 000000000..05970924b --- /dev/null +++ b/libs/common/src/lib/interfaces/timeline-position.interface.ts @@ -0,0 +1,23 @@ +import { + MarketState, + Type +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { Currency } from '@prisma/client'; +import Big from 'big.js'; + +export interface TimelinePosition { + averagePrice: Big; + currency: Currency; + firstBuyDate: string; + marketState: MarketState; + quantity: Big; + symbol: string; + investment: Big; + grossPerformancePercentage: Big; + grossPerformance: Big; + marketPrice: number; + transactionCount: number; + name: string; + url: string; + type: Type; +}