Browse Source

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.
pull/4056/head
Brandon Wortman 9 months ago
parent
commit
f5e92a663d
  1. 58
      apps/client/src/app/services/import-activities.service.ts

58
apps/client/src/app/services/import-activities.service.ts

@ -1,14 +1,14 @@
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; 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 { parseDate as parseDateHelper } from '@ghostfolio/common/helper';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { Account, DataSource, Type as ActivityType } from '@prisma/client';
import { isFinite } from 'lodash'; import { isFinite } from 'lodash';
import { parse as csvToJson } from 'papaparse'; import { parse as csvToJson } from 'papaparse';
import { EMPTY } from 'rxjs'; import { EMPTY, map, firstValueFrom } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@Injectable({ @Injectable({
@ -19,12 +19,13 @@ export class ImportActivitiesService {
private static COMMENT_KEYS = ['comment', 'note']; private static COMMENT_KEYS = ['comment', 'note'];
private static CURRENCY_KEYS = ['ccy', 'currency', 'currencyprimary']; private static CURRENCY_KEYS = ['ccy', 'currency', 'currencyprimary'];
private static DATA_SOURCE_KEYS = ['datasource']; 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 FEE_KEYS = ['commission', 'fee', 'ibcommission'];
private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units']; private static QUANTITY_KEYS = ['qty', 'quantity', 'shares', 'units'];
private static SYMBOL_KEYS = ['code', 'symbol', 'ticker']; private static SYMBOL_KEYS = ['code', 'symbol', 'ticker'];
private static TYPE_KEYS = ['action', 'buy/sell', 'type']; private static TYPE_KEYS = ['action', 'buy/sell', 'type'];
private static UNIT_PRICE_KEYS = [ private static UNIT_PRICE_KEYS = [
'purchase price',
'price', 'price',
'tradeprice', 'tradeprice',
'unitprice', 'unitprice',
@ -55,7 +56,7 @@ export class ImportActivitiesService {
activities.push({ activities.push({
accountId: this.parseAccount({ item, userAccounts }), accountId: this.parseAccount({ item, userAccounts }),
comment: this.parseComment({ item }), comment: this.parseComment({ item }),
currency: this.parseCurrency({ content, index, item }), currency: await this.parseCurrency({ content, index, item }),
dataSource: this.parseDataSource({ item }), dataSource: this.parseDataSource({ item }),
date: this.parseDate({ content, index, item }), date: this.parseDate({ content, index, item }),
fee: this.parseFee({ content, index, item }), fee: this.parseFee({ content, index, item }),
@ -191,7 +192,7 @@ export class ImportActivitiesService {
return undefined; return undefined;
} }
private parseCurrency({ private async parseCurrency({
content, content,
index, index,
item item
@ -202,16 +203,39 @@ export class ImportActivitiesService {
}) { }) {
item = this.lowercaseKeys(item); item = this.lowercaseKeys(item);
//Attempt to find currency in the CSV
for (const key of ImportActivitiesService.CURRENCY_KEYS) { for (const key of ImportActivitiesService.CURRENCY_KEYS) {
if (item[key]) { if (item[key]) {
return item[key]; return item[key];
} }
} }
throw { // If no currency in CSV, make an API request to get symbol data
activities: content, const symbol = this.parseSymbol({ content, index, item });
message: `activities.${index}.currency is not valid` const dataSource = this.parseDataSource({ item }) ?? DataSource.YAHOO;
};
return firstValueFrom(this.http
.get<SymbolItem>(`/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 }) { private parseDataSource({ item }: { item: any }) {
@ -252,7 +276,7 @@ export class ImportActivitiesService {
} }
private parseFee({ private parseFee({
content, // content,
index, index,
item item
}: { }: {
@ -268,10 +292,8 @@ export class ImportActivitiesService {
} }
} }
throw { console.warn(`activities.${index}.fee was not provided, defaulting to 0`);
activities: content, return 0;
message: `activities.${index}.fee is not valid`
};
} }
private parseQuantity({ private parseQuantity({
@ -321,7 +343,7 @@ export class ImportActivitiesService {
} }
private parseType({ private parseType({
content, //content,
index, index,
item item
}: { }: {
@ -354,10 +376,8 @@ export class ImportActivitiesService {
} }
} }
throw { console.warn(`activities.${index}.type was not provided, defaulting to BUY`);
activities: content, return 'BUY';
message: `activities.${index}.type is not valid`
};
} }
private parseUnitPrice({ private parseUnitPrice({

Loading…
Cancel
Save