parent
b5f565c054
commit
a850e8ca22
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for importing dividends from a data provider
|
||||||
|
|
||||||
## 1.224.0 - 2023-01-04
|
## 1.224.0 - 2023-01-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
import { ImportResponse } from '@ghostfolio/common/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
Logger,
|
Logger,
|
||||||
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
UseGuards
|
UseGuards,
|
||||||
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { ImportDataDto } from './import-data.dto';
|
import { ImportDataDto } from './import-data.dto';
|
||||||
import { ImportService } from './import.service';
|
import { ImportService } from './import.service';
|
||||||
@ -74,4 +81,23 @@ export class ImportController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('dividends/:dataSource/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async gatherDividends(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<ImportResponse> {
|
||||||
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
|
||||||
|
const activities = await this.importService.getDividends({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
userCurrency
|
||||||
|
});
|
||||||
|
|
||||||
|
return { activities };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
||||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ImportController } from './import.controller';
|
import { ImportController } from './import.controller';
|
||||||
@ -22,8 +24,10 @@ import { ImportService } from './import.service';
|
|||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisCacheModule
|
RedisCacheModule,
|
||||||
|
SymbolProfileModule
|
||||||
],
|
],
|
||||||
providers: [ImportService]
|
providers: [ImportService]
|
||||||
})
|
})
|
||||||
|
@ -2,9 +2,16 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
|||||||
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 { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import {
|
||||||
|
AccountWithPlatform,
|
||||||
|
OrderWithAccount
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -17,9 +24,81 @@ export class ImportService {
|
|||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly orderService: OrderService
|
private readonly orderService: OrderService,
|
||||||
|
private readonly portfolioService: PortfolioService,
|
||||||
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
userCurrency
|
||||||
|
}: UniqueAsset & { userCurrency: string }): Promise<Activity[]> {
|
||||||
|
try {
|
||||||
|
const { firstBuyDate, historicalData, orders } =
|
||||||
|
await this.portfolioService.getPosition(dataSource, undefined, symbol);
|
||||||
|
|
||||||
|
const [[assetProfile], dividends] = await Promise.all([
|
||||||
|
this.symbolProfileService.getSymbolProfiles([
|
||||||
|
{
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
await this.dataProviderService.getDividends({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
from: parseDate(firstBuyDate),
|
||||||
|
granularity: 'day',
|
||||||
|
to: new Date()
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const accounts = orders.map((order) => {
|
||||||
|
return order.Account;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Account = this.isUniqueAccount(accounts) ? accounts[0] : undefined;
|
||||||
|
|
||||||
|
return Object.entries(dividends).map(([dateString, { marketPrice }]) => {
|
||||||
|
const quantity =
|
||||||
|
historicalData.find((historicalDataItem) => {
|
||||||
|
return historicalDataItem.date === dateString;
|
||||||
|
})?.quantity ?? 0;
|
||||||
|
|
||||||
|
const value = new Big(quantity).mul(marketPrice).toNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
Account,
|
||||||
|
quantity,
|
||||||
|
value,
|
||||||
|
accountId: Account?.id,
|
||||||
|
accountUserId: undefined,
|
||||||
|
comment: undefined,
|
||||||
|
createdAt: undefined,
|
||||||
|
date: parseDate(dateString),
|
||||||
|
fee: 0,
|
||||||
|
feeInBaseCurrency: 0,
|
||||||
|
id: assetProfile.id,
|
||||||
|
isDraft: false,
|
||||||
|
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
|
||||||
|
symbolProfileId: assetProfile.id,
|
||||||
|
type: 'DIVIDEND',
|
||||||
|
unitPrice: marketPrice,
|
||||||
|
updatedAt: undefined,
|
||||||
|
userId: Account?.userId,
|
||||||
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
|
value,
|
||||||
|
assetProfile.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async import({
|
public async import({
|
||||||
activitiesDto,
|
activitiesDto,
|
||||||
isDryRun = false,
|
isDryRun = false,
|
||||||
@ -161,6 +240,16 @@ export class ImportService {
|
|||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUniqueAccount(accounts: AccountWithPlatform[]) {
|
||||||
|
const uniqueAccountIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
uniqueAccountIds.add(account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueAccountIds.size === 1;
|
||||||
|
}
|
||||||
|
|
||||||
private async validateActivities({
|
private async validateActivities({
|
||||||
activitiesDto,
|
activitiesDto,
|
||||||
maxActivitiesToImport,
|
maxActivitiesToImport,
|
||||||
|
@ -660,8 +660,9 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].SymbolProfile.currency;
|
const positionCurrency = orders[0].SymbolProfile.currency;
|
||||||
const [SymbolProfile] =
|
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||||
await this.symbolProfileService.getSymbolProfilesBySymbols([aSymbol]);
|
{ dataSource: aDataSource, symbol: aSymbol }
|
||||||
|
]);
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders
|
const portfolioOrders: PortfolioOrder[] = orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
@ -745,6 +746,7 @@ export class PortfolioService {
|
|||||||
historicalDataArray.push({
|
historicalDataArray.push({
|
||||||
averagePrice: orders[0].unitPrice,
|
averagePrice: orders[0].unitPrice,
|
||||||
date: firstBuyDate,
|
date: firstBuyDate,
|
||||||
|
quantity: orders[0].quantity,
|
||||||
value: orders[0].unitPrice
|
value: orders[0].unitPrice
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -761,6 +763,7 @@ export class PortfolioService {
|
|||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
let currentAveragePrice = 0;
|
let currentAveragePrice = 0;
|
||||||
|
let currentQuantity = 0;
|
||||||
const currentSymbol = transactionPoints[j].items.find(
|
const currentSymbol = transactionPoints[j].items.find(
|
||||||
(item) => item.symbol === aSymbol
|
(item) => item.symbol === aSymbol
|
||||||
);
|
);
|
||||||
@ -768,11 +771,13 @@ export class PortfolioService {
|
|||||||
currentAveragePrice = currentSymbol.quantity.eq(0)
|
currentAveragePrice = currentSymbol.quantity.eq(0)
|
||||||
? 0
|
? 0
|
||||||
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
|
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
|
||||||
|
currentQuantity = currentSymbol.quantity.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
historicalDataArray.push({
|
historicalDataArray.push({
|
||||||
date,
|
date,
|
||||||
averagePrice: currentAveragePrice,
|
averagePrice: currentAveragePrice,
|
||||||
|
quantity: currentQuantity,
|
||||||
value: marketPrice
|
value: marketPrice
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,6 +37,20 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -59,6 +59,10 @@ import { DataProviderService } from './data-provider.service';
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
exports: [DataProviderService, GhostfolioScraperApiService]
|
exports: [
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class DataProviderModule {}
|
export class DataProviderModule {}
|
||||||
|
@ -23,6 +23,27 @@ export class DataProviderService {
|
|||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
dataSource,
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return this.getDataProvider(DataSource[dataSource]).getDividends({
|
||||||
|
from,
|
||||||
|
granularity,
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aItems: IDataGatheringItem[],
|
aItems: IDataGatheringItem[],
|
||||||
aGranularity: Granularity = 'month',
|
aGranularity: Granularity = 'month',
|
||||||
|
@ -37,6 +37,20 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -37,6 +37,20 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -34,6 +34,20 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -11,6 +11,18 @@ export interface DataProviderInterface {
|
|||||||
|
|
||||||
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
|
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
|
||||||
|
|
||||||
|
getDividends({
|
||||||
|
from,
|
||||||
|
granularity,
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}): Promise<{ [date: string]: IDataProviderHistoricalResponse }>;
|
||||||
|
|
||||||
getHistorical(
|
getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity,
|
aGranularity: Granularity,
|
||||||
|
@ -29,6 +29,20 @@ export class ManualService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -31,6 +31,20 @@ export class RapidApiService implements DataProviderInterface {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
|
@ -160,6 +160,59 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDividends({
|
||||||
|
from,
|
||||||
|
granularity = 'day',
|
||||||
|
symbol,
|
||||||
|
to
|
||||||
|
}: {
|
||||||
|
from: Date;
|
||||||
|
granularity: Granularity;
|
||||||
|
symbol: string;
|
||||||
|
to: Date;
|
||||||
|
}) {
|
||||||
|
if (isSameDay(from, to)) {
|
||||||
|
to = addDays(to, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const historicalResult = await yahooFinance.historical(
|
||||||
|
this.convertToYahooFinanceSymbol(symbol),
|
||||||
|
{
|
||||||
|
events: 'dividends',
|
||||||
|
interval: granularity === 'month' ? '1mo' : '1d',
|
||||||
|
period1: format(from, DATE_FORMAT),
|
||||||
|
period2: format(to, DATE_FORMAT)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: {
|
||||||
|
[date: string]: IDataProviderHistoricalResponse;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
for (const historicalItem of historicalResult) {
|
||||||
|
response[format(historicalItem.date, DATE_FORMAT)] = {
|
||||||
|
marketPrice: this.getConvertedValue({
|
||||||
|
symbol,
|
||||||
|
value: historicalItem.dividends
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
|
||||||
|
from,
|
||||||
|
DATE_FORMAT
|
||||||
|
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
|
||||||
|
'YahooFinanceService'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbol: string,
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
@ -172,11 +225,9 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
to = addDays(to, 1);
|
to = addDays(to, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalResult = await yahooFinance.historical(
|
const historicalResult = await yahooFinance.historical(
|
||||||
yahooFinanceSymbol,
|
this.convertToYahooFinanceSymbol(aSymbol),
|
||||||
{
|
{
|
||||||
interval: '1d',
|
interval: '1d',
|
||||||
period1: format(from, DATE_FORMAT),
|
period1: format(from, DATE_FORMAT),
|
||||||
@ -188,27 +239,14 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// Convert symbol back
|
response[aSymbol] = {};
|
||||||
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
|
||||||
|
|
||||||
response[symbol] = {};
|
|
||||||
|
|
||||||
for (const historicalItem of historicalResult) {
|
for (const historicalItem of historicalResult) {
|
||||||
let marketPrice = historicalItem.close;
|
response[aSymbol][format(historicalItem.date, DATE_FORMAT)] = {
|
||||||
|
marketPrice: this.getConvertedValue({
|
||||||
if (symbol === `${this.baseCurrency}GBp`) {
|
symbol: aSymbol,
|
||||||
// Convert GPB to GBp (pence)
|
value: historicalItem.close
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
}),
|
||||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
|
||||||
// Convert ILS to ILA
|
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
|
||||||
} else if (symbol === `${this.baseCurrency}ZAc`) {
|
|
||||||
// Convert ZAR to ZAc (cents)
|
|
||||||
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
|
|
||||||
marketPrice,
|
|
||||||
performance: historicalItem.open - historicalItem.close
|
performance: historicalItem.open - historicalItem.close
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -423,6 +461,27 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return name || shortName || symbol;
|
return name || shortName || symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getConvertedValue({
|
||||||
|
symbol,
|
||||||
|
value
|
||||||
|
}: {
|
||||||
|
symbol: string;
|
||||||
|
value: number;
|
||||||
|
}) {
|
||||||
|
if (symbol === `${this.baseCurrency}GBp`) {
|
||||||
|
// Convert GPB to GBp (pence)
|
||||||
|
return new Big(value).mul(100).toNumber();
|
||||||
|
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||||
|
// Convert ILS to ILA
|
||||||
|
return new Big(value).mul(100).toNumber();
|
||||||
|
} else if (symbol === `${this.baseCurrency}ZAc`) {
|
||||||
|
// Convert ZAR to ZAc (cents)
|
||||||
|
return new Big(value).mul(100).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private parseAssetClass(aPrice: Price): {
|
private parseAssetClass(aPrice: Price): {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
|
@ -11,7 +11,7 @@ import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
|||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
@ -198,6 +198,24 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onImportDividends() {
|
||||||
|
const dialogRef = this.dialog.open(ImportActivitiesDialog, {
|
||||||
|
data: <ImportActivitiesDialogParams>{
|
||||||
|
activityTypes: ['DIVIDEND'],
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
user: this.user
|
||||||
|
},
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.fetchActivities();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onUpdateActivity(aActivity: OrderModel) {
|
public onUpdateActivity(aActivity: OrderModel) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { activityId: aActivity.id, editDialog: true }
|
queryParams: { activityId: aActivity.id, editDialog: true }
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
(export)="onExport($event)"
|
(export)="onExport($event)"
|
||||||
(exportDrafts)="onExportDrafts($event)"
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
|
(importDividends)="onImportDividends()"
|
||||||
></gf-activities-table>
|
></gf-activities-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,12 +5,16 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
OnDestroy
|
OnDestroy
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
import { isArray } from 'lodash';
|
import { Position } from '@ghostfolio/common/interfaces';
|
||||||
import { Subject } from 'rxjs';
|
import { AssetClass } from '@prisma/client';
|
||||||
|
import { isArray, sortBy } from 'lodash';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -24,20 +28,55 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
public activities: Activity[] = [];
|
public activities: Activity[] = [];
|
||||||
public details: any[] = [];
|
public details: any[] = [];
|
||||||
public errorMessages: string[] = [];
|
public errorMessages: string[] = [];
|
||||||
|
public holdings: Position[] = [];
|
||||||
public isFileSelected = false;
|
public isFileSelected = false;
|
||||||
|
public mode: 'DIVIDEND';
|
||||||
public selectedActivities: Activity[] = [];
|
public selectedActivities: Activity[] = [];
|
||||||
|
public uniqueAssetForm: FormGroup;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
|
||||||
|
private dataService: DataService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
public dialogRef: MatDialogRef<ImportActivitiesDialog>,
|
public dialogRef: MatDialogRef<ImportActivitiesDialog>,
|
||||||
private importActivitiesService: ImportActivitiesService,
|
private importActivitiesService: ImportActivitiesService,
|
||||||
private snackBar: MatSnackBar
|
private snackBar: MatSnackBar
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {
|
||||||
|
this.uniqueAssetForm = this.formBuilder.group({
|
||||||
|
uniqueAsset: [undefined, Validators.required]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.data?.activityTypes?.length === 1 &&
|
||||||
|
this.data?.activityTypes?.[0] === 'DIVIDEND'
|
||||||
|
) {
|
||||||
|
this.mode = 'DIVIDEND';
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPositions({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
id: AssetClass.EQUITY,
|
||||||
|
type: 'ASSET_CLASS'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
range: 'max'
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ positions }) => {
|
||||||
|
this.holdings = sortBy(positions, ({ name }) => {
|
||||||
|
return name.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
@ -71,6 +110,24 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onLoadDividends() {
|
||||||
|
const { dataSource, symbol } =
|
||||||
|
this.uniqueAssetForm.controls['uniqueAsset'].value;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchDividendsImport({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ activities }) => {
|
||||||
|
this.activities = activities;
|
||||||
|
this.isFileSelected = true;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onReset() {
|
public onReset() {
|
||||||
this.details = [];
|
this.details = [];
|
||||||
this.errorMessages = [];
|
this.errorMessages = [];
|
||||||
@ -95,8 +152,6 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
reader.onload = async (readerEvent) => {
|
reader.onload = async (readerEvent) => {
|
||||||
const fileContent = readerEvent.target.result as string;
|
const fileContent = readerEvent.target.result as string;
|
||||||
|
|
||||||
console.log(fileContent);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (file.name.endsWith('.json')) {
|
if (file.name.endsWith('.json')) {
|
||||||
const content = JSON.parse(fileContent);
|
const content = JSON.parse(fileContent);
|
||||||
|
@ -7,31 +7,59 @@
|
|||||||
|
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<ng-container *ngIf="!isFileSelected">
|
<ng-container *ngIf="!isFileSelected">
|
||||||
<div class="d-flex justify-content-center flex-column">
|
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
|
||||||
<button
|
<form [formGroup]="uniqueAssetForm" (ngSubmit)="onLoadDividends()">
|
||||||
class="py-3"
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
color="primary"
|
<mat-label i18n>Holding</mat-label>
|
||||||
mat-stroked-button
|
<mat-select formControlName="uniqueAsset">
|
||||||
(click)="onSelectFile()"
|
<mat-option
|
||||||
>
|
*ngFor="let holding of holdings"
|
||||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
[value]="{dataSource: holding.dataSource, symbol: holding.symbol}"
|
||||||
<span i18n>Choose File</span>
|
>{{ holding.name }}</mat-option
|
||||||
</button>
|
>
|
||||||
<p class="mb-0 mt-4 text-center">
|
</mat-select>
|
||||||
<span class="mr-1" i18n>The following file formats are supported:</span>
|
</mat-form-field>
|
||||||
<a
|
<div class="d-flex justify-content-center flex-column">
|
||||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
<button
|
||||||
target="_blank"
|
color="primary"
|
||||||
>CSV</a
|
mat-flat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="!uniqueAssetForm.valid"
|
||||||
|
>
|
||||||
|
<span i18n>Load Dividends</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #selectFile>
|
||||||
|
<div class="d-flex justify-content-center flex-column">
|
||||||
|
<button
|
||||||
|
class="py-3"
|
||||||
|
color="primary"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="onSelectFile()"
|
||||||
>
|
>
|
||||||
<span class="mx-1" i18n>or</span>
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
<a
|
<span i18n>Choose File</span>
|
||||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
</button>
|
||||||
target="_blank"
|
<p class="mb-0 mt-4 text-center">
|
||||||
>JSON</a
|
<span class="mr-1" i18n
|
||||||
>
|
>The following file formats are supported:</span
|
||||||
</p>
|
>
|
||||||
</div>
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||||
|
target="_blank"
|
||||||
|
>CSV</a
|
||||||
|
>
|
||||||
|
<span class="mx-1" i18n>or</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||||
|
target="_blank"
|
||||||
|
>JSON</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="isFileSelected">
|
<ng-container *ngIf="isFileSelected">
|
||||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||||
@ -47,6 +75,7 @@
|
|||||||
[locale]="data?.user?.settings?.locale"
|
[locale]="data?.user?.settings?.locale"
|
||||||
[showActions]="false"
|
[showActions]="false"
|
||||||
[showCheckbox]="true"
|
[showCheckbox]="true"
|
||||||
|
[showFooter]="false"
|
||||||
[showSymbolColumn]="false"
|
[showSymbolColumn]="false"
|
||||||
(selectedActivities)="updateSelection($event)"
|
(selectedActivities)="updateSelection($event)"
|
||||||
></gf-activities-table>
|
></gf-activities-table>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatExpansionModule } from '@angular/material/expansion';
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
@ -13,12 +16,16 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
|||||||
declarations: [ImportActivitiesDialog],
|
declarations: [ImportActivitiesDialog],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
GfActivitiesTableModule,
|
GfActivitiesTableModule,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
GfDialogHeaderModule,
|
GfDialogHeaderModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatExpansionModule
|
MatExpansionModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Type } from '@prisma/client';
|
||||||
|
|
||||||
export interface ImportActivitiesDialogParams {
|
export interface ImportActivitiesDialogParams {
|
||||||
|
activityTypes: Type[];
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
Export,
|
Export,
|
||||||
Filter,
|
Filter,
|
||||||
|
ImportResponse,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
OAuthResponse,
|
OAuthResponse,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
@ -119,6 +120,12 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchDividendsImport({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
return this.http.get<ImportResponse>(
|
||||||
|
`/api/v1/import/dividends/${dataSource}/${symbol}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchExchangeRateForDate({
|
public fetchExchangeRateForDate({
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
|
@ -90,13 +90,16 @@ export class ImportActivitiesService {
|
|||||||
selectedActivities: Activity[]
|
selectedActivities: Activity[]
|
||||||
): Promise<Activity[]> {
|
): Promise<Activity[]> {
|
||||||
const importData: CreateOrderDto[] = [];
|
const importData: CreateOrderDto[] = [];
|
||||||
|
|
||||||
for (const activity of selectedActivities) {
|
for (const activity of selectedActivities) {
|
||||||
importData.push(this.convertToCreateOrderDto(activity));
|
importData.push(this.convertToCreateOrderDto(activity));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.importJson({ content: importData });
|
return this.importJson({ content: importData });
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertToCreateOrderDto({
|
private convertToCreateOrderDto({
|
||||||
|
accountId,
|
||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
@ -105,6 +108,7 @@ export class ImportActivitiesService {
|
|||||||
unitPrice
|
unitPrice
|
||||||
}: Activity): CreateOrderDto {
|
}: Activity): CreateOrderDto {
|
||||||
return {
|
return {
|
||||||
|
accountId,
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
type,
|
type,
|
||||||
|
@ -8,7 +8,7 @@ export interface EnhancedSymbolProfile {
|
|||||||
activitiesCount: number;
|
activitiesCount: number;
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
comment?: string;
|
comment: string | null;
|
||||||
countries: Country[];
|
countries: Country[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
currency: string | null;
|
currency: string | null;
|
||||||
|
@ -4,6 +4,7 @@ export interface HistoricalDataItem {
|
|||||||
grossPerformancePercent?: number;
|
grossPerformancePercent?: number;
|
||||||
netPerformance?: number;
|
netPerformance?: number;
|
||||||
netPerformanceInPercentage?: number;
|
netPerformanceInPercentage?: number;
|
||||||
|
quantity?: number;
|
||||||
totalInvestment?: number;
|
totalInvestment?: number;
|
||||||
value?: number;
|
value?: number;
|
||||||
}
|
}
|
||||||
|
3
libs/common/src/lib/types/account-with-platform.type.ts
Normal file
3
libs/common/src/lib/types/account-with-platform.type.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Account, Platform } from '@prisma/client';
|
||||||
|
|
||||||
|
export type AccountWithPlatform = Account & { Platform?: Platform };
|
@ -1,4 +1,5 @@
|
|||||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||||
|
import { AccountWithPlatform } from './account-with-platform.type';
|
||||||
import { AccountWithValue } from './account-with-value.type';
|
import { AccountWithValue } from './account-with-value.type';
|
||||||
import type { ColorScheme } from './color-scheme';
|
import type { ColorScheme } from './color-scheme';
|
||||||
import type { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
@ -13,6 +14,7 @@ import type { ViewMode } from './view-mode.type';
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
|
AccountWithPlatform,
|
||||||
AccountWithValue,
|
AccountWithValue,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Account, Order, Platform, SymbolProfile, Tag } from '@prisma/client';
|
import { Order, SymbolProfile, Tag } from '@prisma/client';
|
||||||
|
|
||||||
type AccountWithPlatform = Account & { Platform?: Platform };
|
import { AccountWithPlatform } from './account-with-platform.type';
|
||||||
|
|
||||||
export type OrderWithAccount = Order & {
|
export type OrderWithAccount = Order & {
|
||||||
Account?: AccountWithPlatform;
|
Account?: AccountWithPlatform;
|
||||||
|
@ -117,7 +117,7 @@
|
|||||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-truncate">{{ element.SymbolProfile.name }}</span>
|
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
|
||||||
<span
|
<span
|
||||||
*ngIf="element.isDraft"
|
*ngIf="element.isDraft"
|
||||||
class="badge badge-secondary ml-1"
|
class="badge badge-secondary ml-1"
|
||||||
@ -126,9 +126,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!isUUID(element.SymbolProfile.symbol)">
|
<div *ngIf="!isUUID(element.SymbolProfile?.symbol)">
|
||||||
<small class="text-muted">{{
|
<small class="text-muted">{{
|
||||||
element.SymbolProfile.symbol | gfSymbol
|
element.SymbolProfile?.symbol | gfSymbol
|
||||||
}}</small>
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -149,7 +149,7 @@
|
|||||||
class="d-none d-lg-table-cell px-1"
|
class="d-none d-lg-table-cell px-1"
|
||||||
mat-cell
|
mat-cell
|
||||||
>
|
>
|
||||||
{{ element.SymbolProfile.currency }}
|
{{ element.SymbolProfile?.currency }}
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
{{ baseCurrency }}
|
{{ baseCurrency }}
|
||||||
@ -388,6 +388,14 @@
|
|||||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
<span i18n>Import Activities</span>
|
<span i18n>Import Activities</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToImportActivities"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onImportDividends()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||||
|
<span i18n>Import Dividends</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="hasPermissionToExportActivities"
|
*ngIf="hasPermissionToExportActivities"
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
@ -459,7 +467,10 @@
|
|||||||
<tr
|
<tr
|
||||||
*matFooterRowDef="displayedColumns"
|
*matFooterRowDef="displayedColumns"
|
||||||
mat-footer-row
|
mat-footer-row
|
||||||
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
|
[ngClass]="{
|
||||||
|
'd-none':
|
||||||
|
isLoading || dataSource.data.length === 0 || showFooter === false
|
||||||
|
}"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,8 +41,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() hasPermissionToOpenDetails = true;
|
@Input() hasPermissionToOpenDetails = true;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions = true;
|
||||||
@Input() showCheckbox = false;
|
@Input() showCheckbox = false;
|
||||||
|
@Input() showFooter = true;
|
||||||
@Input() showNameColumn = true;
|
@Input() showNameColumn = true;
|
||||||
|
|
||||||
@Output() activityDeleted = new EventEmitter<string>();
|
@Output() activityDeleted = new EventEmitter<string>();
|
||||||
@ -51,6 +52,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Output() export = new EventEmitter<string[]>();
|
@Output() export = new EventEmitter<string[]>();
|
||||||
@Output() exportDrafts = new EventEmitter<string[]>();
|
@Output() exportDrafts = new EventEmitter<string[]>();
|
||||||
@Output() import = new EventEmitter<void>();
|
@Output() import = new EventEmitter<void>();
|
||||||
|
@Output() importDividends = new EventEmitter<UniqueAsset>();
|
||||||
@Output() selectedActivities = new EventEmitter<Activity[]>();
|
@Output() selectedActivities = new EventEmitter<Activity[]>();
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ -233,6 +235,10 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
this.import.emit();
|
this.import.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onImportDividends() {
|
||||||
|
this.importDividends.emit();
|
||||||
|
}
|
||||||
|
|
||||||
public onOpenComment(aComment: string) {
|
public onOpenComment(aComment: string) {
|
||||||
alert(aComment);
|
alert(aComment);
|
||||||
}
|
}
|
||||||
@ -272,13 +278,18 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValueMap[activity.SymbolProfile.currency] = {
|
if (activity.SymbolProfile?.currency) {
|
||||||
id: activity.SymbolProfile.currency,
|
fieldValueMap[activity.SymbolProfile.currency] = {
|
||||||
label: activity.SymbolProfile.currency,
|
id: activity.SymbolProfile.currency,
|
||||||
type: 'TAG'
|
label: activity.SymbolProfile.currency,
|
||||||
};
|
type: 'TAG'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!isUUID(activity.SymbolProfile.symbol)) {
|
if (
|
||||||
|
activity.SymbolProfile?.symbol &&
|
||||||
|
!isUUID(activity.SymbolProfile.symbol)
|
||||||
|
) {
|
||||||
fieldValueMap[activity.SymbolProfile.symbol] = {
|
fieldValueMap[activity.SymbolProfile.symbol] = {
|
||||||
id: activity.SymbolProfile.symbol,
|
id: activity.SymbolProfile.symbol,
|
||||||
label: activity.SymbolProfile.symbol,
|
label: activity.SymbolProfile.symbol,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user