Implement new positions endpoint
This commit is contained in:
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…
x
Reference in New Issue
Block a user