|
|
|
@ -8,10 +8,14 @@ import {
|
|
|
|
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|
|
|
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
|
|
|
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
|
|
|
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
|
|
|
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
|
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
|
|
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
|
|
|
|
import { parseDate } from '@ghostfolio/common/helper';
|
|
|
|
|
import {
|
|
|
|
|
getAssetProfileIdentifier,
|
|
|
|
|
parseDate
|
|
|
|
|
} from '@ghostfolio/common/helper';
|
|
|
|
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
|
|
|
|
import {
|
|
|
|
|
AccountWithPlatform,
|
|
|
|
@ -21,12 +25,14 @@ import { Injectable } from '@nestjs/common';
|
|
|
|
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
|
|
|
|
import Big from 'big.js';
|
|
|
|
|
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
|
|
|
|
|
import { uniqBy } from 'lodash';
|
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ImportService {
|
|
|
|
|
public constructor(
|
|
|
|
|
private readonly accountService: AccountService,
|
|
|
|
|
private readonly dataGatheringService: DataGatheringService,
|
|
|
|
|
private readonly dataProviderService: DataProviderService,
|
|
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
|
|
|
private readonly orderService: OrderService,
|
|
|
|
@ -220,8 +226,7 @@ export class ImportService {
|
|
|
|
|
|
|
|
|
|
const assetProfiles = await this.validateActivities({
|
|
|
|
|
activitiesDto,
|
|
|
|
|
maxActivitiesToImport,
|
|
|
|
|
userId
|
|
|
|
|
maxActivitiesToImport
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
|
|
|
@ -250,10 +255,37 @@ export class ImportService {
|
|
|
|
|
error,
|
|
|
|
|
fee,
|
|
|
|
|
quantity,
|
|
|
|
|
SymbolProfile: assetProfile,
|
|
|
|
|
SymbolProfile,
|
|
|
|
|
type,
|
|
|
|
|
unitPrice
|
|
|
|
|
} of activitiesExtendedWithErrors) {
|
|
|
|
|
const assetProfile = assetProfiles[
|
|
|
|
|
getAssetProfileIdentifier({
|
|
|
|
|
dataSource: SymbolProfile.dataSource,
|
|
|
|
|
symbol: SymbolProfile.symbol
|
|
|
|
|
})
|
|
|
|
|
] ?? {
|
|
|
|
|
currency: SymbolProfile.currency,
|
|
|
|
|
dataSource: SymbolProfile.dataSource,
|
|
|
|
|
symbol: SymbolProfile.symbol
|
|
|
|
|
};
|
|
|
|
|
const {
|
|
|
|
|
assetClass,
|
|
|
|
|
assetSubClass,
|
|
|
|
|
countries,
|
|
|
|
|
createdAt,
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
id,
|
|
|
|
|
isin,
|
|
|
|
|
name,
|
|
|
|
|
scraperConfiguration,
|
|
|
|
|
sectors,
|
|
|
|
|
symbol,
|
|
|
|
|
symbolMapping,
|
|
|
|
|
url,
|
|
|
|
|
updatedAt
|
|
|
|
|
} = assetProfile;
|
|
|
|
|
const validatedAccount = accounts.find(({ id }) => {
|
|
|
|
|
return id === accountId;
|
|
|
|
|
});
|
|
|
|
@ -279,23 +311,22 @@ export class ImportService {
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
isDraft: isAfter(date, endOfToday()),
|
|
|
|
|
SymbolProfile: {
|
|
|
|
|
assetClass: assetProfile.assetClass,
|
|
|
|
|
assetSubClass: assetProfile.assetSubClass,
|
|
|
|
|
comment: assetProfile.comment,
|
|
|
|
|
countries: assetProfile.countries,
|
|
|
|
|
createdAt: assetProfile.createdAt,
|
|
|
|
|
currency: assetProfile.currency,
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
id: assetProfile.id,
|
|
|
|
|
isin: assetProfile.isin,
|
|
|
|
|
name: assetProfile.name,
|
|
|
|
|
scraperConfiguration: assetProfile.scraperConfiguration,
|
|
|
|
|
sectors: assetProfile.sectors,
|
|
|
|
|
symbol: assetProfile.currency,
|
|
|
|
|
symbolMapping: assetProfile.symbolMapping,
|
|
|
|
|
updatedAt: assetProfile.updatedAt,
|
|
|
|
|
url: assetProfile.url,
|
|
|
|
|
...assetProfiles[assetProfile.symbol]
|
|
|
|
|
assetClass,
|
|
|
|
|
assetSubClass,
|
|
|
|
|
countries,
|
|
|
|
|
createdAt,
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
id,
|
|
|
|
|
isin,
|
|
|
|
|
name,
|
|
|
|
|
scraperConfiguration,
|
|
|
|
|
sectors,
|
|
|
|
|
symbol,
|
|
|
|
|
symbolMapping,
|
|
|
|
|
updatedAt,
|
|
|
|
|
url,
|
|
|
|
|
comment: assetProfile.comment
|
|
|
|
|
},
|
|
|
|
|
Account: validatedAccount,
|
|
|
|
|
symbolProfileId: undefined,
|
|
|
|
@ -318,14 +349,14 @@ export class ImportService {
|
|
|
|
|
SymbolProfile: {
|
|
|
|
|
connectOrCreate: {
|
|
|
|
|
create: {
|
|
|
|
|
currency: assetProfile.currency,
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
symbol: assetProfile.symbol
|
|
|
|
|
currency,
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
},
|
|
|
|
|
where: {
|
|
|
|
|
dataSource_symbol: {
|
|
|
|
|
dataSource: assetProfile.dataSource,
|
|
|
|
|
symbol: assetProfile.symbol
|
|
|
|
|
dataSource,
|
|
|
|
|
symbol
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -337,24 +368,49 @@ export class ImportService {
|
|
|
|
|
|
|
|
|
|
const value = new Big(quantity).mul(unitPrice).toNumber();
|
|
|
|
|
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
activities.push({
|
|
|
|
|
...order,
|
|
|
|
|
error,
|
|
|
|
|
value,
|
|
|
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
|
fee,
|
|
|
|
|
assetProfile.currency,
|
|
|
|
|
currency,
|
|
|
|
|
userCurrency
|
|
|
|
|
),
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
SymbolProfile: assetProfile,
|
|
|
|
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
|
|
|
|
value,
|
|
|
|
|
assetProfile.currency,
|
|
|
|
|
currency,
|
|
|
|
|
userCurrency
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
activities.sort((activity1, activity2) => {
|
|
|
|
|
return Number(activity1.date) - Number(activity2.date);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!isDryRun) {
|
|
|
|
|
// Gather symbol data in the background, if not dry run
|
|
|
|
|
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => {
|
|
|
|
|
return getAssetProfileIdentifier({
|
|
|
|
|
dataSource: SymbolProfile.dataSource,
|
|
|
|
|
symbol: SymbolProfile.symbol
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.dataGatheringService.gatherSymbols(
|
|
|
|
|
uniqueActivities.map(({ date, SymbolProfile }) => {
|
|
|
|
|
return {
|
|
|
|
|
date,
|
|
|
|
|
dataSource: SymbolProfile.dataSource,
|
|
|
|
|
symbol: SymbolProfile.symbol
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return activities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -446,25 +502,30 @@ export class ImportService {
|
|
|
|
|
|
|
|
|
|
private async validateActivities({
|
|
|
|
|
activitiesDto,
|
|
|
|
|
maxActivitiesToImport,
|
|
|
|
|
userId
|
|
|
|
|
maxActivitiesToImport
|
|
|
|
|
}: {
|
|
|
|
|
activitiesDto: Partial<CreateOrderDto>[];
|
|
|
|
|
maxActivitiesToImport: number;
|
|
|
|
|
userId: string;
|
|
|
|
|
}) {
|
|
|
|
|
if (activitiesDto?.length > maxActivitiesToImport) {
|
|
|
|
|
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const assetProfiles: {
|
|
|
|
|
[symbol: string]: Partial<SymbolProfile>;
|
|
|
|
|
[assetProfileIdentifier: string]: Partial<SymbolProfile>;
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
const uniqueActivitiesDto = uniqBy(
|
|
|
|
|
activitiesDto,
|
|
|
|
|
({ dataSource, symbol }) => {
|
|
|
|
|
return getAssetProfileIdentifier({ dataSource, symbol });
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const [
|
|
|
|
|
index,
|
|
|
|
|
{ currency, dataSource, symbol }
|
|
|
|
|
] of activitiesDto.entries()) {
|
|
|
|
|
] of uniqueActivitiesDto.entries()) {
|
|
|
|
|
if (dataSource !== 'MANUAL') {
|
|
|
|
|
const assetProfile = (
|
|
|
|
|
await this.dataProviderService.getAssetProfiles([
|
|
|
|
@ -484,7 +545,8 @@ export class ImportService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assetProfiles[symbol] = assetProfile;
|
|
|
|
|
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =
|
|
|
|
|
assetProfile;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|