From f5e92a663d015d5fd000d7129336264c21f2f4d2 Mon Sep 17 00:00:00 2001 From: Brandon Wortman Date: Sun, 17 Nov 2024 17:25:49 -0500 Subject: [PATCH] Support CSV file format of Yahoo Finance in activities import #2288 Rearranged date keys to prioritize trade date over date due to both provided in Yahoo CSV. If no currency is found in CSV make API call to gather currency info for symbol. If no fee provided, default to 0 (fee free). If no type provided, default to 'BUY'. Next: provide UI warnings for both above. --- .../app/services/import-activities.service.ts | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index c1b2209b3..b52a0b2f2 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -1,14 +1,14 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { parseDate as parseDateHelper } from '@ghostfolio/common/helper'; - import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { isFinite } from 'lodash'; import { parse as csvToJson } from 'papaparse'; -import { EMPTY } from 'rxjs'; +import { EMPTY, map, firstValueFrom } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable({ @@ -19,12 +19,13 @@ export class ImportActivitiesService { private static COMMENT_KEYS = ['comment', 'note']; private static CURRENCY_KEYS = ['ccy', 'currency', 'currencyprimary']; private static DATA_SOURCE_KEYS = ['datasource']; - private static DATE_KEYS = ['date', 'tradedate']; + private static DATE_KEYS = ['trade date', 'tradedate', 'date']; private static FEE_KEYS = ['commission', 'fee', 'ibcommission']; private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units']; private static SYMBOL_KEYS = ['code', 'symbol', 'ticker']; private static TYPE_KEYS = ['action', 'buy/sell', 'type']; private static UNIT_PRICE_KEYS = [ + 'purchase price', 'price', 'tradeprice', 'unitprice', @@ -55,7 +56,7 @@ export class ImportActivitiesService { activities.push({ accountId: this.parseAccount({ item, userAccounts }), comment: this.parseComment({ item }), - currency: this.parseCurrency({ content, index, item }), + currency: await this.parseCurrency({ content, index, item }), dataSource: this.parseDataSource({ item }), date: this.parseDate({ content, index, item }), fee: this.parseFee({ content, index, item }), @@ -191,7 +192,7 @@ export class ImportActivitiesService { return undefined; } - private parseCurrency({ + private async parseCurrency({ content, index, item @@ -202,16 +203,39 @@ export class ImportActivitiesService { }) { item = this.lowercaseKeys(item); + //Attempt to find currency in the CSV for (const key of ImportActivitiesService.CURRENCY_KEYS) { if (item[key]) { return item[key]; } } - throw { - activities: content, - message: `activities.${index}.currency is not valid` - }; + // If no currency in CSV, make an API request to get symbol data + const symbol = this.parseSymbol({ content, index, item }); + const dataSource = this.parseDataSource({ item }) ?? DataSource.YAHOO; + + return firstValueFrom(this.http + .get(`/api/v1/symbol/${dataSource}/${symbol}`) + .pipe( + map((response) => { + if (response?.currency) { + console.warn(`activities.${index}.currency was not provided, using ${response.currency} from symbol data`); + return response.currency; + } + throw { + activities: content, + message: `activities.${index}.currency is not valid` + }; + }), + catchError((error) => { + console.warn("Failed to fetch currency from symbol service.", error); + throw { + activities: content, + message: `activities.${index}.currency is not valid` + }; + }) + ) + ); } private parseDataSource({ item }: { item: any }) { @@ -252,7 +276,7 @@ export class ImportActivitiesService { } private parseFee({ - content, + // content, index, item }: { @@ -268,10 +292,8 @@ export class ImportActivitiesService { } } - throw { - activities: content, - message: `activities.${index}.fee is not valid` - }; + console.warn(`activities.${index}.fee was not provided, defaulting to 0`); + return 0; } private parseQuantity({ @@ -321,7 +343,7 @@ export class ImportActivitiesService { } private parseType({ - content, + //content, index, item }: { @@ -354,10 +376,8 @@ export class ImportActivitiesService { } } - throw { - activities: content, - message: `activities.${index}.type is not valid` - }; + console.warn(`activities.${index}.type was not provided, defaulting to BUY`); + return 'BUY'; } private parseUnitPrice({