Feature/support manual currency for fee (#1490)
* Support manual currency for fee * Update changelog
This commit is contained in:
parent
d58400788a
commit
292d345ce0
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Supported a note for asset profiles
|
- Supported a note for asset profiles
|
||||||
|
- Supported a manual currency for the activity fee
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import { AppController } from './app.controller';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
|
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
|
||||||
import { ExportModule } from './export/export.module';
|
import { ExportModule } from './export/export.module';
|
||||||
import { FrontendMiddleware } from './frontend.middleware';
|
import { FrontendMiddleware } from './frontend.middleware';
|
||||||
import { ImportModule } from './import/import.module';
|
import { ImportModule } from './import/import.module';
|
||||||
@ -52,6 +53,7 @@ import { UserModule } from './user/user.module';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
ExchangeRateModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ExportModule,
|
ExportModule,
|
||||||
ImportModule,
|
ImportModule,
|
||||||
|
26
apps/api/src/app/exchange-rate/exchange-rate.controller.ts
Normal file
26
apps/api/src/app/exchange-rate/exchange-rate.controller.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import { ExchangeRateService } from './exchange-rate.service';
|
||||||
|
|
||||||
|
@Controller('exchange-rate')
|
||||||
|
export class ExchangeRateController {
|
||||||
|
public constructor(
|
||||||
|
private readonly exchangeRateService: ExchangeRateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get(':symbol/:dateString')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getExchangeRate(
|
||||||
|
@Param('dateString') dateString: string,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<IDataProviderHistoricalResponse> {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
return this.exchangeRateService.getExchangeRate({
|
||||||
|
date,
|
||||||
|
symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
13
apps/api/src/app/exchange-rate/exchange-rate.module.ts
Normal file
13
apps/api/src/app/exchange-rate/exchange-rate.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ExchangeRateController } from './exchange-rate.controller';
|
||||||
|
import { ExchangeRateService } from './exchange-rate.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ExchangeRateController],
|
||||||
|
exports: [ExchangeRateService],
|
||||||
|
imports: [ExchangeRateDataModule],
|
||||||
|
providers: [ExchangeRateService]
|
||||||
|
})
|
||||||
|
export class ExchangeRateModule {}
|
29
apps/api/src/app/exchange-rate/exchange-rate.service.ts
Normal file
29
apps/api/src/app/exchange-rate/exchange-rate.service.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExchangeRateService {
|
||||||
|
public constructor(
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getExchangeRate({
|
||||||
|
date,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
date: Date;
|
||||||
|
symbol: string;
|
||||||
|
}): Promise<IDataProviderHistoricalResponse> {
|
||||||
|
const [currency1, currency2] = symbol.split('-');
|
||||||
|
|
||||||
|
const marketPrice = await this.exchangeRateDataService.toCurrencyAtDate(
|
||||||
|
1,
|
||||||
|
currency1,
|
||||||
|
currency2,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
|
||||||
|
return { marketPrice };
|
||||||
|
}
|
||||||
|
}
|
@ -78,6 +78,7 @@ describe('CurrentRateService', () => {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
marketDataService = new MarketDataService(null);
|
marketDataService = new MarketDataService(null);
|
||||||
|
@ -91,10 +91,19 @@ export class SymbolController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.symbolService.getForDate({
|
const result = await this.symbolService.getForDate({
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result || isEmpty(result)) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data.service'
|
|||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { format, subDays } from 'date-fns';
|
import { format, subDays } from 'date-fns';
|
||||||
|
|
||||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||||
@ -65,13 +64,9 @@ export class SymbolService {
|
|||||||
|
|
||||||
public async getForDate({
|
public async getForDate({
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date = new Date(),
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
|
||||||
dataSource: DataSource;
|
|
||||||
date: Date;
|
|
||||||
symbol: string;
|
|
||||||
}): Promise<IDataProviderHistoricalResponse> {
|
|
||||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||||
[{ dataSource, symbol }],
|
[{ dataSource, symbol }],
|
||||||
date,
|
date,
|
||||||
|
@ -114,9 +114,13 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allData = await Promise.all(promises);
|
try {
|
||||||
for (const { data, symbol } of allData) {
|
const allData = await Promise.all(promises);
|
||||||
result[symbol] = data;
|
for (const { data, symbol } of allData) {
|
||||||
|
result[symbol] = data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error, 'DataProviderService');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -209,7 +213,9 @@ export class DataProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
`Fetched ${symbolsChunk.length} quotes from ${dataSource} in ${(
|
`Fetched ${symbolsChunk.length} quote${
|
||||||
|
symbolsChunk.length > 1 ? 's' : ''
|
||||||
|
} from ${dataSource} in ${(
|
||||||
(performance.now() - startTimeDataSource) /
|
(performance.now() - startTimeDataSource) /
|
||||||
1000
|
1000
|
||||||
).toFixed(3)} seconds`
|
).toFixed(3)} seconds`
|
||||||
@ -223,7 +229,7 @@ export class DataProviderService {
|
|||||||
|
|
||||||
Logger.debug('------------------------------------------------');
|
Logger.debug('------------------------------------------------');
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
`Fetched ${items.length} quotes in ${(
|
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
|
||||||
(performance.now() - startTimeTotal) /
|
(performance.now() - startTimeTotal) /
|
||||||
1000
|
1000
|
||||||
).toFixed(3)} seconds`
|
).toFixed(3)} seconds`
|
||||||
|
@ -4,16 +4,18 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MarketDataModule } from './market-data.module';
|
||||||
import { PrismaModule } from './prisma.module';
|
import { PrismaModule } from './prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
exports: [ExchangeRateDataService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PropertyModule
|
PropertyModule
|
||||||
],
|
],
|
||||||
providers: [ExchangeRateDataService],
|
providers: [ExchangeRateDataService]
|
||||||
exports: [ExchangeRateDataService]
|
|
||||||
})
|
})
|
||||||
export class ExchangeRateDataModule {}
|
export class ExchangeRateDataModule {}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format, isToday } from 'date-fns';
|
||||||
import { isNumber, uniq } from 'lodash';
|
import { isNumber, uniq } from 'lodash';
|
||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { ConfigurationService } from './configuration.service';
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
|
import { MarketDataService } from './market-data.service';
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from './prisma.service';
|
||||||
import { PropertyService } from './property/property.service';
|
import { PropertyService } from './property/property.service';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export class ExchangeRateDataService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService
|
private readonly propertyService: PropertyService
|
||||||
) {}
|
) {}
|
||||||
@ -152,6 +154,53 @@ export class ExchangeRateDataService {
|
|||||||
return aValue;
|
return aValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async toCurrencyAtDate(
|
||||||
|
aValue: number,
|
||||||
|
aFromCurrency: string,
|
||||||
|
aToCurrency: string,
|
||||||
|
aDate: Date
|
||||||
|
) {
|
||||||
|
if (aValue === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToday(aDate)) {
|
||||||
|
return this.toCurrency(aValue, aFromCurrency, aToCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
let factor = 1;
|
||||||
|
|
||||||
|
if (aFromCurrency !== aToCurrency) {
|
||||||
|
const dataSource = this.dataProviderService.getPrimaryDataSource();
|
||||||
|
const symbol = `${aFromCurrency}${aToCurrency}`;
|
||||||
|
|
||||||
|
const marketData = await this.marketDataService.get({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: aDate
|
||||||
|
});
|
||||||
|
|
||||||
|
if (marketData?.marketPrice) {
|
||||||
|
factor = marketData?.marketPrice;
|
||||||
|
} else {
|
||||||
|
// TODO: Get from data provider service or calculate indirectly via base currency
|
||||||
|
// and market data
|
||||||
|
return this.toCurrency(aValue, aFromCurrency, aToCurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(factor) && !isNaN(factor)) {
|
||||||
|
return factor * aValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback with error, if currencies are not available
|
||||||
|
Logger.error(
|
||||||
|
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`,
|
||||||
|
'ExchangeRateDataService'
|
||||||
|
);
|
||||||
|
return aValue;
|
||||||
|
}
|
||||||
|
|
||||||
private async prepareCurrencies(): Promise<string[]> {
|
private async prepareCurrencies(): Promise<string[]> {
|
||||||
let currencies: string[] = [];
|
let currencies: string[] = [];
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, MarketData, Prisma } from '@prisma/client';
|
import { DataSource, MarketData, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
@ -20,14 +22,13 @@ export class MarketDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async get({
|
public async get({
|
||||||
date,
|
dataSource,
|
||||||
|
date = new Date(),
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: IDataGatheringItem): Promise<MarketData> {
|
||||||
date: Date;
|
|
||||||
symbol: string;
|
|
||||||
}): Promise<MarketData> {
|
|
||||||
return await this.prismaService.marketData.findFirst({
|
return await this.prismaService.marketData.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: resetHours(date)
|
date: resetHours(date)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import { translate } from '@ghostfolio/ui/i18n';
|
|||||||
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
import { AssetClass, AssetSubClass, Type } from '@prisma/client';
|
||||||
import { isUUID } from 'class-validator';
|
import { isUUID } from 'class-validator';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { EMPTY, Observable, Subject } from 'rxjs';
|
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@ -86,12 +86,17 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.data.activity?.SymbolProfile?.currency,
|
this.data.activity?.SymbolProfile?.currency,
|
||||||
Validators.required
|
Validators.required
|
||||||
],
|
],
|
||||||
|
currencyOfFee: [
|
||||||
|
this.data.activity?.SymbolProfile?.currency,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
dataSource: [
|
dataSource: [
|
||||||
this.data.activity?.SymbolProfile?.dataSource,
|
this.data.activity?.SymbolProfile?.dataSource,
|
||||||
Validators.required
|
Validators.required
|
||||||
],
|
],
|
||||||
date: [this.data.activity?.date, Validators.required],
|
date: [this.data.activity?.date, Validators.required],
|
||||||
fee: [this.data.activity?.fee, Validators.required],
|
fee: [this.data.activity?.fee, Validators.required],
|
||||||
|
feeInCustomCurrency: [this.data.activity?.fee, Validators.required],
|
||||||
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
|
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
|
||||||
quantity: [this.data.activity?.quantity, Validators.required],
|
quantity: [this.data.activity?.quantity, Validators.required],
|
||||||
searchSymbol: [
|
searchSymbol: [
|
||||||
@ -108,7 +113,36 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
|
|
||||||
this.activityForm.valueChanges
|
this.activityForm.valueChanges
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.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(
|
||||||
|
this.dataService
|
||||||
|
.fetchExchangeRateForDate({
|
||||||
|
date,
|
||||||
|
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 (
|
if (
|
||||||
this.activityForm.controls['type'].value === 'BUY' ||
|
this.activityForm.controls['type'].value === 'BUY' ||
|
||||||
this.activityForm.controls['type'].value === 'ITEM'
|
this.activityForm.controls['type'].value === 'ITEM'
|
||||||
@ -123,6 +157,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.activityForm.controls['unitPrice'].value -
|
this.activityForm.controls['unitPrice'].value -
|
||||||
this.activityForm.controls['fee'].value ?? 0;
|
this.activityForm.controls['fee'].value ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filteredLookupItemsObservable = this.activityForm.controls[
|
this.filteredLookupItemsObservable = this.activityForm.controls[
|
||||||
@ -160,6 +196,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.activityForm.controls['currency'].setValue(
|
this.activityForm.controls['currency'].setValue(
|
||||||
this.data.user.settings.baseCurrency
|
this.data.user.settings.baseCurrency
|
||||||
);
|
);
|
||||||
|
this.activityForm.controls['currencyOfFee'].setValue(
|
||||||
|
this.data.user.settings.baseCurrency
|
||||||
|
);
|
||||||
this.activityForm.controls['dataSource'].removeValidators(
|
this.activityForm.controls['dataSource'].removeValidators(
|
||||||
Validators.required
|
Validators.required
|
||||||
);
|
);
|
||||||
@ -189,6 +228,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
);
|
);
|
||||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activityForm.controls['type'].setValue(this.data.activity?.type);
|
this.activityForm.controls['type'].setValue(this.data.activity?.type);
|
||||||
@ -313,6 +354,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe(({ currency, dataSource, marketPrice }) => {
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
this.activityForm.controls['currency'].setValue(currency);
|
this.activityForm.controls['currency'].setValue(currency);
|
||||||
|
this.activityForm.controls['currencyOfFee'].setValue(currency);
|
||||||
this.activityForm.controls['dataSource'].setValue(dataSource);
|
this.activityForm.controls['dataSource'].setValue(dataSource);
|
||||||
|
|
||||||
this.currentMarketPrice = marketPrice;
|
this.currentMarketPrice = marketPrice;
|
||||||
|
@ -127,6 +127,23 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<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-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Fee</mat-label>
|
<mat-label i18n>Fee</mat-label>
|
||||||
<input formControlName="fee" matInput type="number" />
|
<input formControlName="fee" matInput type="number" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
|
||||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||||
|
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
|
@ -12,6 +12,7 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in
|
|||||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
@ -36,12 +37,7 @@ import {
|
|||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import {
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
AssetClass,
|
|
||||||
AssetSubClass,
|
|
||||||
DataSource,
|
|
||||||
Order as OrderModel
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { cloneDeep, groupBy } from 'lodash';
|
import { cloneDeep, groupBy } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -104,6 +100,18 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchExchangeRateForDate({
|
||||||
|
date,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
date: Date;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.http.get<IDataProviderHistoricalResponse>(
|
||||||
|
`/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteAccess(aId: string) {
|
public deleteAccess(aId: string) {
|
||||||
return this.http.delete<any>(`/api/v1/access/${aId}`);
|
return this.http.delete<any>(`/api/v1/access/${aId}`);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user