create investment endpoint for analysis timeline

Co-authored-by: Thomas <dotsilver@gmail.com>
pull/239/head
Valentin Zickner 3 years ago committed by Thomas
parent 4a0695613e
commit de83dc7b84

@ -311,6 +311,23 @@ export class PortfolioCalculator {
}; };
} }
public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) {
return [];
}
return this.transactionPoints.map((transactionPoint) => {
return {
date: transactionPoint.date,
investment: transactionPoint.items.reduce(
(investment, transactionPointSymbol) =>
investment.add(transactionPointSymbol.investment),
new Big(0)
)
};
});
}
public async calculateTimeline( public async calculateTimeline(
timelineSpecification: TimelineSpecification[], timelineSpecification: TimelineSpecification[],
endDate: string endDate: string

@ -39,6 +39,7 @@ import {
} from './interfaces/portfolio-position-detail.interface'; } from './interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from './interfaces/portfolio-positions.interface'; import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
import { PortfolioService } from './portfolio.service'; import { PortfolioService } from './portfolio.service';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@Controller('portfolio') @Controller('portfolio')
export class PortfolioController { export class PortfolioController {
@ -49,12 +50,14 @@ export class PortfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@Get() @Get('/investments')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async findAll( public async findAll(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<PortfolioItem[]> { ): Promise<InvestmentItem[]> {
let portfolio = await this.portfolioService.findAll(impersonationId); let investments = await this.portfolioService.getInvestments(
impersonationId
);
if ( if (
impersonationId && impersonationId &&
@ -63,25 +66,18 @@ export class PortfolioController {
permissions.readForeignPortfolio permissions.readForeignPortfolio
) )
) { ) {
portfolio = portfolio.map((portfolioItem) => { const maxInvestment = investments.reduce(
Object.keys(portfolioItem.positions).forEach((symbol) => { (investment, item) => Math.max(investment, item.investment),
portfolioItem.positions[symbol].investment = 1
portfolioItem.positions[symbol].investment > 0 ? 1 : 0; );
portfolioItem.positions[symbol].investmentInOriginalCurrency =
portfolioItem.positions[symbol].investmentInOriginalCurrency > 0 investments = investments.map((item) => ({
? 1 date: item.date,
: 0; investment: item.investment / maxInvestment
portfolioItem.positions[symbol].quantity = }));
portfolioItem.positions[symbol].quantity > 0 ? 1 : 0;
});
portfolioItem.investment = null;
return portfolioItem;
});
} }
return portfolio; return investments;
} }
@Get('chart') @Get('chart')

@ -63,6 +63,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface'; import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
@ -136,34 +137,35 @@ export class PortfolioService {
return portfolio; return portfolio;
} }
public async findAll(aImpersonationId: string): Promise<PortfolioItem[]> { public async getInvestments(
try { aImpersonationId: string
const impersonationUserId = ): Promise<InvestmentItem[]> {
await this.impersonationService.validateImpersonationId( const userId = await this.getUserId(aImpersonationId);
aImpersonationId,
this.request.user.id
);
const portfolio = await this.createPortfolio( const portfolioCalculator = new PortfolioCalculator(
impersonationUserId || this.request.user.id this.currentRateService,
); this.request.user.Settings.currency
return portfolio.get(); );
} catch (error) {
console.error(error); const { transactionPoints } = await this.getTransactionPoints(userId);
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return [];
} }
return portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
} }
public async getChart( public async getChart(
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<HistoricalDataItem[]> { ): Promise<HistoricalDataItem[]> {
const impersonationUserId = const userId = await this.getUserId(aImpersonationId);
await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const userId = impersonationUserId || this.request.user.id;
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator(
this.currentRateService, this.currentRateService,

@ -10,16 +10,16 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { primaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb } from '@ghostfolio/common/config';
import { PortfolioItem } from '@ghostfolio/common/interfaces';
import { import {
Chart,
LinearScale,
LineController, LineController,
LineElement, LineElement,
LinearScale,
PointElement, PointElement,
TimeScale TimeScale
} from 'chart.js'; } from 'chart.js';
import { Chart } from 'chart.js';
import { addMonths, isAfter, parseISO, subMonths } from 'date-fns'; import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@Component({ @Component({
selector: 'gf-investment-chart', selector: 'gf-investment-chart',
@ -28,7 +28,7 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
styleUrls: ['./investment-chart.component.scss'] styleUrls: ['./investment-chart.component.scss']
}) })
export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
@Input() portfolioItems: PortfolioItem[]; @Input() investments: InvestmentItem[];
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas;
@ -48,7 +48,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
if (this.portfolioItems) { if (this.investments) {
this.initialize(); this.initialize();
} }
} }
@ -60,32 +60,32 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
private initialize() { private initialize() {
this.isLoading = true; this.isLoading = true;
if (this.portfolioItems?.length > 0) { if (this.investments?.length > 0) {
// Extend chart by three months (before) // Extend chart by three months (before)
const firstItem = this.portfolioItems[0]; const firstItem = this.investments[0];
this.portfolioItems.unshift({ this.investments.unshift({
...firstItem, ...firstItem,
date: subMonths(parseISO(firstItem.date), 3).toISOString(), date: subMonths(parseISO(firstItem.date), 3).toISOString(),
investment: 0 investment: 0
}); });
// Extend chart by three months (after) // Extend chart by three months (after)
const lastItem = this.portfolioItems[this.portfolioItems.length - 1]; const lastItem = this.investments[this.investments.length - 1];
this.portfolioItems.push({ this.investments.push({
...lastItem, ...lastItem,
date: addMonths(parseISO(lastItem.date), 3).toISOString() date: addMonths(parseISO(lastItem.date), 3).toISOString()
}); });
} }
const data = { const data = {
labels: this.portfolioItems.map((position) => { labels: this.investments.map((position) => {
return position.date; return position.date;
}), }),
datasets: [ datasets: [
{ {
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2, borderWidth: 2,
data: this.portfolioItems.map((position) => { data: this.investments.map((position) => {
return position.investment; return position.investment;
}), }),
segment: { segment: {

@ -12,6 +12,7 @@ import {
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@Component({ @Component({
selector: 'gf-analysis-page', selector: 'gf-analysis-page',
@ -35,7 +36,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
{ label: 'Initial', value: 'original' }, { label: 'Initial', value: 'original' },
{ label: 'Current', value: 'current' } { label: 'Current', value: 'current' }
]; ];
public portfolioItems: PortfolioItem[]; public investments: InvestmentItem[];
public portfolioPositions: { [symbol: string]: PortfolioPosition }; public portfolioPositions: { [symbol: string]: PortfolioPosition };
public positions: { [symbol: string]: any }; public positions: { [symbol: string]: any };
public positionsArray: PortfolioPosition[]; public positionsArray: PortfolioPosition[];
@ -71,10 +72,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}); });
this.dataService this.dataService
.fetchPortfolio() .fetchInvestments()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe((response) => {
this.portfolioItems = response; this.investments = response;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

@ -202,7 +202,7 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-investment-chart <gf-investment-chart
[portfolioItems]="portfolioItems" [investments]='investments'
></gf-investment-chart> ></gf-investment-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

@ -36,6 +36,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { SettingsStorageService } from './settings-storage.service'; import { SettingsStorageService } from './settings-storage.service';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -143,8 +144,8 @@ export class DataService {
); );
} }
public fetchPortfolio() { public fetchInvestments() {
return this.http.get<PortfolioItem[]>('/api/portfolio'); return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
} }
public fetchPortfolioOverview() { public fetchPortfolioOverview() {

@ -0,0 +1,4 @@
export interface InvestmentItem {
date: string;
investment: number;
}
Loading…
Cancel
Save