Implement new positions endpoint

pull/239/head
Thomas 3 years ago
parent 8a482e63b9
commit b4dc21dd61

@ -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'
}
});
});

@ -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 {

@ -0,0 +1,5 @@
import { TimelinePosition } from '@ghostfolio/common/interfaces';
export interface PortfolioPositions {
positions: TimelinePosition[];
}

@ -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 <any>res.json(performance);
}
@Get('positions')
@UseGuards(AuthGuard('jwt'))
public async getPositions(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioPositions> {
const positions = await this.portfolioService.getPositions(impersonationId);
return { positions };
}
@Get('position/:symbol')
@UseGuards(AuthGuard('jwt'))
public async getPosition(

@ -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: <OrderType>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: <OrderType>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<PortfolioOverview> {

@ -11,7 +11,7 @@
[isLoading]="isLoading"
[marketState]="position?.marketState"
[range]="range"
[value]="position?.grossPerformancePercent"
[value]="position?.grossPerformancePercentage"
></gf-trend-indicator>
</div>
<div *ngIf="isLoading" class="flex-grow-1">
@ -53,7 +53,7 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="position?.grossPerformancePercent"
[value]="position?.grossPerformancePercentage"
></gf-value>
</div>
</div>

@ -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;

@ -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;
}

@ -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;

@ -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);
}

@ -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<SymbolItem>(`/api/symbol/${aSymbol}`);
}
public fetchPositions(): Observable<PortfolioPositions> {
return this.http.get<PortfolioPositions>('/api/portfolio/positions').pipe(
map((respose) => {
return respose;
})
);
}
public fetchSymbols(aQuery: string) {
return this.http
.get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`)

@ -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

@ -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;
}
Loading…
Cancel
Save