Feature/support update of historical data (#557)

* Support update of historical data

* Update changelog
pull/558/head
Thomas Kaul 2 years ago committed by GitHub
parent aca0d77e91
commit ddce8cc7f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a line chart to the historical data view in the admin control panel
- Supported the update of historical data in the admin control panel
## 1.91.0 - 18.12.2021

@ -1,6 +1,6 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
AdminData,
AdminMarketData,
@ -22,16 +22,18 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource, MarketData } from '@prisma/client';
import { isDate, isValid } from 'date-fns';
import { isDate } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service';
import { UpdateMarketDataDto } from './update-market-data.dto';
@Controller('admin')
export class AdminController {
public constructor(
private readonly adminService: AdminService,
private readonly dataGatheringService: DataGatheringService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@ -173,7 +175,7 @@ export class AdminController {
@Get('market-data/:symbol')
@UseGuards(AuthGuard('jwt'))
public async getMarketDataBySymbol(
@Param('symbol') symbol
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
if (
!hasPermission(
@ -190,6 +192,39 @@ export class AdminController {
return this.adminService.getMarketDataBySymbol(symbol);
}
@Put('market-data/:dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
public async update(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string,
@Body() data: UpdateMarketDataDto
) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const date = new Date(dateString);
return this.marketDataService.updateMarketData({
data,
where: {
date_symbol: {
date,
symbol
}
}
});
}
@Put('settings/:key')
@UseGuards(AuthGuard('jwt'))
public async updateProperty(

@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator';
export class UpdateMarketDataDto {
@IsNumber()
marketPrice: number;
}

@ -1,3 +1,4 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -12,9 +13,9 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { DataSource, MarketData } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { isDate, isEmpty } from 'lodash';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -78,4 +79,27 @@ export class SymbolController {
return result;
}
@Get(':dataSource/:symbol/:dateString')
@UseGuards(AuthGuard('jwt'))
public async gatherSymbolForDate(
@Param('dataSource') dataSource: DataSource,
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<IDataProviderHistoricalResponse> {
const date = new Date(dateString);
if (!isDate(date)) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
return this.symbolService.getForDate({
dataSource,
date,
symbol
});
}
}

@ -1,11 +1,15 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { subDays } from 'date-fns';
import { DataSource, MarketData } from '@prisma/client';
import { format, subDays } from 'date-fns';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -58,6 +62,27 @@ export class SymbolService {
return undefined;
}
public async getForDate({
dataSource,
date,
symbol
}: {
dataSource: DataSource;
date: Date;
symbol: string;
}): Promise<IDataProviderHistoricalResponse> {
const historicalData = await this.dataProviderService.getHistoricalRaw(
[{ dataSource, symbol }],
date,
date
);
return {
marketPrice:
historicalData?.[symbol]?.[format(date, DATE_FORMAT)]?.marketPrice
};
}
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] };

@ -1,4 +1,5 @@
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceService } from './yahoo-finance.service';
jest.mock(

@ -1,6 +1,6 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { baseCurrency, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';

@ -65,4 +65,16 @@ export class MarketDataService {
where
});
}
public async updateMarketData(params: {
data: Prisma.MarketDataUpdateInput;
where: Prisma.MarketDataWhereUniqueInput;
}): Promise<MarketData> {
const { data, where } = params;
return this.prismaService.marketData.update({
data,
where
});
}
}

@ -1,9 +1,11 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnInit
OnInit,
Output
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
@ -27,6 +29,8 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
@Input() marketData: MarketData[];
@Input() symbol: string;
@Output() marketDataChanged = new EventEmitter<boolean>();
public days = Array(31);
public defaultDateFormat = DEFAULT_DATE_FORMAT;
public deviceType: string;
@ -101,7 +105,9 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
.subscribe(({ withRefresh }) => {
this.marketDataChanged.next(withRefresh);
});
}
public ngOnDestroy() {

@ -7,7 +7,6 @@ import {
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { MarketData } from '@prisma/client';
import { Subject, takeUntil } from 'rxjs';
import { MarketDataDetailDialogParams } from './interfaces/interfaces';
@ -32,24 +31,38 @@ export class MarketDataDetailDialog implements OnDestroy {
public ngOnInit() {}
public onCancel(): void {
this.dialogRef.close();
this.dialogRef.close({ withRefresh: false });
}
public onGatherData() {
public onFetchSymbolForDate() {
this.adminService
.gatherSymbol({
.fetchSymbolForDate({
dataSource: this.data.dataSource,
date: this.data.date,
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((marketData: MarketData) => {
this.data.marketPrice = marketData.marketPrice;
.subscribe(({ marketPrice }) => {
this.data.marketPrice = marketPrice;
this.changeDetectorRef.markForCheck();
});
}
public onUpdate() {
this.adminService
.putMarketData({
dataSource: this.data.dataSource,
date: this.data.date,
marketData: { marketPrice: this.data.marketPrice },
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.dialogRef.close({ withRefresh: true });
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

@ -21,22 +21,30 @@
<mat-datepicker #date disabled="true"></mat-datepicker>
</mat-form-field>
</div>
<div class="align-items-center d-flex">
<mat-form-field appearance="outline" class="flex-grow-1 mr-2">
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Market Price</mat-label>
<input
matInput
name="marketPrice"
readonly
type="number"
[(ngModel)]="data.marketPrice"
/>
<button
mat-icon-button
matSuffix
title="Fetch market price"
(click)="onFetchSymbolForDate()"
>
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
</button>
</mat-form-field>
<button color="accent" i18n mat-flat-button (click)="onGatherData()">
Gather Data
</button>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button>
<button color="primary" i18n mat-flat-button (click)="onUpdate()">
Save
</button>
</div>
</form>

@ -68,6 +68,13 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}
}
public onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) {
this.fetchAdminMarketData();
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

@ -47,6 +47,7 @@
[dataSource]="item.dataSource"
[marketData]="marketDataDetails"
[symbol]="item.symbol"
(marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail>
</td>
</tr>

@ -131,7 +131,7 @@
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="unit-price w-100">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Unit Price</mat-label>
<input
matInput

@ -1,5 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns';
@ -35,4 +37,40 @@ export class AdminService {
return this.http.post<MarketData | void>(url, {});
}
public fetchSymbolForDate({
dataSource,
date,
symbol
}: {
dataSource: DataSource;
date: Date;
symbol: string;
}) {
const url = `/api/symbol/${dataSource}/${symbol}/${format(
date,
DATE_FORMAT
)}`;
return this.http.get<IDataProviderHistoricalResponse>(url);
}
public putMarketData({
dataSource,
date,
marketData,
symbol
}: {
dataSource: DataSource;
date: Date;
marketData: UpdateMarketDataDto;
symbol: string;
}) {
const url = `/api/admin/market-data/${dataSource}/${symbol}/${format(
date,
DATE_FORMAT
)}`;
return this.http.put<MarketData>(url, marketData);
}
}

Loading…
Cancel
Save