Browse Source

Support manual currency for fee

pull/1490/head
Thomas 3 years ago
parent
commit
b6887a74e8
  1. 11
      apps/api/src/app/symbol/symbol.controller.ts
  2. 16
      apps/api/src/services/data-provider/data-provider.service.ts
  3. 10
      apps/api/src/services/market-data.service.ts
  4. 50
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  5. 17
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

11
apps/api/src/app/symbol/symbol.controller.ts

@ -91,10 +91,19 @@ export class SymbolController {
);
}
return this.symbolService.getForDate({
const result = await this.symbolService.getForDate({
dataSource,
date,
symbol
});
if (!result || isEmpty(result)) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return result;
}
}

16
apps/api/src/services/data-provider/data-provider.service.ts

@ -114,9 +114,13 @@ export class DataProviderService {
}
}
const allData = await Promise.all(promises);
for (const { data, symbol } of allData) {
result[symbol] = data;
try {
const allData = await Promise.all(promises);
for (const { data, symbol } of allData) {
result[symbol] = data;
}
} catch (error) {
Logger.error(error, 'DataProviderService');
}
return result;
@ -209,7 +213,9 @@ export class DataProviderService {
}
Logger.debug(
`Fetched ${symbolsChunk.length} quotes from ${dataSource} in ${(
`Fetched ${symbolsChunk.length} quote${
symbolsChunk.length > 1 ? 's' : ''
} from ${dataSource} in ${(
(performance.now() - startTimeDataSource) /
1000
).toFixed(3)} seconds`
@ -223,7 +229,7 @@ export class DataProviderService {
Logger.debug('------------------------------------------------');
Logger.debug(
`Fetched ${items.length} quotes in ${(
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
(performance.now() - startTimeTotal) /
1000
).toFixed(3)} seconds`

10
apps/api/src/services/market-data.service.ts

@ -5,6 +5,7 @@ import { resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { DataSource, MarketData, Prisma } from '@prisma/client';
import { IDataGatheringItem } from './interfaces/interfaces';
@Injectable()
export class MarketDataService {
@ -20,14 +21,13 @@ export class MarketDataService {
}
public async get({
date,
dataSource,
date = new Date(),
symbol
}: {
date: Date;
symbol: string;
}): Promise<MarketData> {
}: IDataGatheringItem): Promise<MarketData> {
return await this.prismaService.marketData.findFirst({
where: {
dataSource,
symbol,
date: resetHours(date)
}

50
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -13,12 +13,13 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
import { isUUID } from 'class-validator';
import { isString } from 'lodash';
import { EMPTY, Observable, Subject } from 'rxjs';
import { EMPTY, lastValueFrom, Observable, Subject } from 'rxjs';
import {
catchError,
debounceTime,
@ -59,6 +60,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams,
private dataService: DataService,
@ -86,12 +88,17 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.data.activity?.SymbolProfile?.currency,
Validators.required
],
currencyOfFee: [
this.data.activity?.SymbolProfile?.currency,
Validators.required
],
dataSource: [
this.data.activity?.SymbolProfile?.dataSource,
Validators.required
],
date: [this.data.activity?.date, Validators.required],
fee: [this.data.activity?.fee, Validators.required],
feeInCustomCurrency: [this.data.activity?.fee, Validators.required],
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [
@ -108,7 +115,38 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
.subscribe(async () => {
let exchangeRate = 1;
const currency = this.activityForm.controls['currency'].value;
const currencyOfFee = this.activityForm.controls['currencyOfFee'].value;
const date = this.activityForm.controls['date'].value;
if (currency && currencyOfFee && currency !== currencyOfFee && date) {
try {
const { marketPrice } = await lastValueFrom(
// TODO: Create endpoint for exchange rate conversion
this.adminService
.fetchSymbolForDate({
date,
dataSource: 'YAHOO',
symbol: `${currencyOfFee}${currency}`
})
.pipe(takeUntil(this.unsubscribeSubject))
);
exchangeRate = marketPrice;
} catch {}
}
const feeInCustomCurrency =
this.activityForm.controls['feeInCustomCurrency'].value *
exchangeRate;
this.activityForm.controls['fee'].setValue(feeInCustomCurrency, {
emitEvent: false
});
if (
this.activityForm.controls['type'].value === 'BUY' ||
this.activityForm.controls['type'].value === 'ITEM'
@ -123,6 +161,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['unitPrice'].value -
this.activityForm.controls['fee'].value ?? 0;
}
this.changeDetectorRef.markForCheck();
});
this.filteredLookupItemsObservable = this.activityForm.controls[
@ -160,6 +200,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['currency'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['currencyOfFee'].setValue(
this.data.user.settings.baseCurrency
);
this.activityForm.controls['dataSource'].removeValidators(
Validators.required
);
@ -189,6 +232,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
);
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
}
this.changeDetectorRef.markForCheck();
});
this.activityForm.controls['type'].setValue(this.data.activity?.type);
@ -313,6 +358,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
)
.subscribe(({ currency, dataSource, marketPrice }) => {
this.activityForm.controls['currency'].setValue(currency);
this.activityForm.controls['currencyOfFee'].setValue(currency);
this.activityForm.controls['dataSource'].setValue(dataSource);
this.currentMarketPrice = marketPrice;

17
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -127,6 +127,23 @@
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input formControlName="feeInCustomCurrency" matInput type="number" />
<div
class="ml-2"
matSuffix
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
>
<mat-select formControlName="currencyOfFee">
<mat-option *ngFor="let currency of currencies" [value]="currency">
{{ currency }}
</mat-option>
</mat-select>
</div>
</mat-form-field>
</div>
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input formControlName="fee" matInput type="number" />

Loading…
Cancel
Save