Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
f46533107d | |||
c216ab1d76 | |||
86acbf06f4 | |||
3de7d3f60e | |||
63ed227f3f | |||
5bb20f6d5f | |||
b3e58d182a | |||
93d6746739 |
29
CHANGELOG.md
29
CHANGELOG.md
@ -5,6 +5,35 @@ 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).
|
||||||
|
|
||||||
|
## 1.123.0 - 05.03.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Included data provider errors in the API response
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed the redundant attributes (`currency`, `dataSource`, `symbol`) of the activity model
|
||||||
|
- Removed the prefix for symbols with the data source `GHOSTFOLIO`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Improved the account calculations
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
|
## 1.122.0 - 01.03.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for click in the portfolio proportion chart component
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with undefined currencies after creating an activity
|
||||||
|
|
||||||
## 1.121.0 - 27.02.2022
|
## 1.121.0 - 27.02.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -101,16 +101,18 @@ export class AccountController {
|
|||||||
) {
|
) {
|
||||||
accountsWithAggregations = {
|
accountsWithAggregations = {
|
||||||
...nullifyValuesInObject(accountsWithAggregations, [
|
...nullifyValuesInObject(accountsWithAggregations, [
|
||||||
'totalBalance',
|
'totalBalanceInBaseCurrency',
|
||||||
'totalValue'
|
'totalValueInBaseCurrency'
|
||||||
]),
|
]),
|
||||||
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||||
'balance',
|
'balance',
|
||||||
|
'balanceInBaseCurrency',
|
||||||
'convertedBalance',
|
'convertedBalance',
|
||||||
'fee',
|
'fee',
|
||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'value'
|
'value',
|
||||||
|
'valueInBaseCurrency'
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
|
|
||||||
@ -105,21 +106,26 @@ export class AccountService {
|
|||||||
aUserId: string,
|
aUserId: string,
|
||||||
aCurrency: string
|
aCurrency: string
|
||||||
): Promise<CashDetails> {
|
): Promise<CashDetails> {
|
||||||
let totalCashBalance = 0;
|
let totalCashBalanceInBaseCurrency = new Big(0);
|
||||||
|
|
||||||
const accounts = await this.accounts({
|
const accounts = await this.accounts({
|
||||||
where: { userId: aUserId }
|
where: { userId: aUserId }
|
||||||
});
|
});
|
||||||
|
|
||||||
accounts.forEach((account) => {
|
for (const account of accounts) {
|
||||||
totalCashBalance += this.exchangeRateDataService.toCurrency(
|
totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus(
|
||||||
account.balance,
|
this.exchangeRateDataService.toCurrency(
|
||||||
account.currency,
|
account.balance,
|
||||||
aCurrency
|
account.currency,
|
||||||
|
aCurrency
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
return { accounts, balance: totalCashBalance };
|
return {
|
||||||
|
accounts,
|
||||||
|
balanceInBaseCurrency: totalCashBalanceInBaseCurrency.toNumber()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAccount(
|
public async updateAccount(
|
||||||
|
@ -2,5 +2,5 @@ import { Account } from '@prisma/client';
|
|||||||
|
|
||||||
export interface CashDetails {
|
export interface CashDetails {
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
balance: number;
|
balanceInBaseCurrency: number;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
AdminMarketDataItem
|
AdminMarketDataItem,
|
||||||
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Property } from '@prisma/client';
|
import { DataSource, Property } from '@prisma/client';
|
||||||
@ -30,13 +31,7 @@ export class AdminService {
|
|||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async deleteProfileData({
|
public async deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
await this.marketDataService.deleteMany({ dataSource, symbol });
|
await this.marketDataService.deleteMany({ dataSource, symbol });
|
||||||
await this.symbolProfileService.delete({ dataSource, symbol });
|
await this.symbolProfileService.delete({ dataSource, symbol });
|
||||||
}
|
}
|
||||||
@ -137,10 +132,7 @@ export class AdminService {
|
|||||||
public async getMarketDataBySymbol({
|
public async getMarketDataBySymbol({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: UniqueAsset): Promise<AdminMarketDataDetails> {
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}): Promise<AdminMarketDataDetails> {
|
|
||||||
return {
|
return {
|
||||||
marketData: await this.marketDataService.marketDataItems({
|
marketData: await this.marketDataService.marketDataItems({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
|
@ -18,8 +18,6 @@ export class ExportService {
|
|||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
select: {
|
select: {
|
||||||
accountId: true,
|
accountId: true,
|
||||||
currency: true,
|
|
||||||
dataSource: true,
|
|
||||||
date: true,
|
date: true,
|
||||||
fee: true,
|
fee: true,
|
||||||
id: true,
|
id: true,
|
||||||
@ -42,7 +40,6 @@ export class ExportService {
|
|||||||
orders: orders.map(
|
orders: orders.map(
|
||||||
({
|
({
|
||||||
accountId,
|
accountId,
|
||||||
currency,
|
|
||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
@ -52,12 +49,12 @@ export class ExportService {
|
|||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
accountId,
|
accountId,
|
||||||
currency,
|
|
||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
type,
|
type,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
dataSource: SymbolProfile.dataSource,
|
dataSource: SymbolProfile.dataSource,
|
||||||
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Order } from '@prisma/client';
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, ValidateNested } from 'class-validator';
|
import { IsArray, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
@ -7,5 +6,5 @@ export class ImportDataDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@Type(() => CreateOrderDto)
|
@Type(() => CreateOrderDto)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
orders: Order[];
|
orders: CreateOrderDto[];
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Order } from '@prisma/client';
|
|
||||||
import { isSameDay, parseISO } from 'date-fns';
|
import { isSameDay, parseISO } from 'date-fns';
|
||||||
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportService {
|
export class ImportService {
|
||||||
@ -19,7 +19,7 @@ export class ImportService {
|
|||||||
orders,
|
orders,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
orders: Partial<Order>[];
|
orders: Partial<CreateOrderDto>[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
@ -52,11 +52,8 @@ export class ImportService {
|
|||||||
unitPrice
|
unitPrice
|
||||||
} of orders) {
|
} of orders) {
|
||||||
await this.orderService.createOrder({
|
await this.orderService.createOrder({
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
fee,
|
fee,
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
|
||||||
type,
|
type,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
userId,
|
userId,
|
||||||
@ -65,6 +62,7 @@ export class ImportService {
|
|||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
create: {
|
create: {
|
||||||
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
},
|
},
|
||||||
@ -85,7 +83,7 @@ export class ImportService {
|
|||||||
orders,
|
orders,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
orders: Partial<Order>[];
|
orders: Partial<CreateOrderDto>[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
if (
|
if (
|
||||||
@ -99,6 +97,7 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingOrders = await this.orderService.orders({
|
const existingOrders = await this.orderService.orders({
|
||||||
|
include: { SymbolProfile: true },
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
where: { userId }
|
where: { userId }
|
||||||
});
|
});
|
||||||
@ -109,12 +108,12 @@ export class ImportService {
|
|||||||
] of orders.entries()) {
|
] of orders.entries()) {
|
||||||
const duplicateOrder = existingOrders.find((order) => {
|
const duplicateOrder = existingOrders.find((order) => {
|
||||||
return (
|
return (
|
||||||
order.currency === currency &&
|
order.SymbolProfile.currency === currency &&
|
||||||
order.dataSource === dataSource &&
|
order.SymbolProfile.dataSource === dataSource &&
|
||||||
isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
|
isSameDay(order.date, parseISO(<string>(<unknown>date))) &&
|
||||||
order.fee === fee &&
|
order.fee === fee &&
|
||||||
order.quantity === quantity &&
|
order.quantity === quantity &&
|
||||||
order.symbol === symbol &&
|
order.SymbolProfile.symbol === symbol &&
|
||||||
order.type === type &&
|
order.type === type &&
|
||||||
order.unitPrice === unitPrice
|
order.unitPrice === unitPrice
|
||||||
);
|
);
|
||||||
|
@ -114,6 +114,7 @@ export class OrderController {
|
|||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
create: {
|
create: {
|
||||||
|
currency: data.currency,
|
||||||
dataSource: data.dataSource,
|
dataSource: data.dataSource,
|
||||||
symbol: data.symbol
|
symbol: data.symbol
|
||||||
},
|
},
|
||||||
@ -171,6 +172,14 @@ export class OrderController {
|
|||||||
id_userId: { id: accountId, userId: this.request.user.id }
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SymbolProfile: {
|
||||||
|
connect: {
|
||||||
|
dataSource_symbol: {
|
||||||
|
dataSource: data.dataSource,
|
||||||
|
symbol: data.symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
|
@ -53,7 +53,13 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async createOrder(
|
public async createOrder(
|
||||||
data: Prisma.OrderCreateInput & { accountId?: string; userId: string }
|
data: Prisma.OrderCreateInput & {
|
||||||
|
accountId?: string;
|
||||||
|
currency?: string;
|
||||||
|
dataSource?: DataSource;
|
||||||
|
symbol?: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
): Promise<Order> {
|
): Promise<Order> {
|
||||||
const defaultAccount = (
|
const defaultAccount = (
|
||||||
await this.accountService.getAccounts(data.userId)
|
await this.accountService.getAccounts(data.userId)
|
||||||
@ -71,15 +77,13 @@ export class OrderService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data.type === 'ITEM') {
|
if (data.type === 'ITEM') {
|
||||||
const currency = data.currency;
|
const currency = data.SymbolProfile.connectOrCreate.create.currency;
|
||||||
const dataSource: DataSource = 'MANUAL';
|
const dataSource: DataSource = 'MANUAL';
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const name = data.SymbolProfile.connectOrCreate.create.symbol;
|
const name = data.SymbolProfile.connectOrCreate.create.symbol;
|
||||||
|
|
||||||
Account = undefined;
|
Account = undefined;
|
||||||
data.dataSource = dataSource;
|
|
||||||
data.id = id;
|
data.id = id;
|
||||||
data.symbol = null;
|
|
||||||
data.SymbolProfile.connectOrCreate.create.currency = currency;
|
data.SymbolProfile.connectOrCreate.create.currency = currency;
|
||||||
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
|
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
|
||||||
data.SymbolProfile.connectOrCreate.create.name = name;
|
data.SymbolProfile.connectOrCreate.create.name = name;
|
||||||
@ -93,29 +97,32 @@ export class OrderService {
|
|||||||
data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase();
|
data.SymbolProfile.connectOrCreate.create.symbol.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.dataGatheringService.gatherProfileData([
|
||||||
|
{
|
||||||
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const isDraft = isAfter(data.date as Date, endOfToday());
|
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||||
|
|
||||||
if (!isDraft) {
|
if (!isDraft) {
|
||||||
// Gather symbol data of order in the background, if not draft
|
// Gather symbol data of order in the background, if not draft
|
||||||
this.dataGatheringService.gatherSymbols([
|
this.dataGatheringService.gatherSymbols([
|
||||||
{
|
{
|
||||||
dataSource: data.dataSource,
|
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
|
||||||
date: <Date>data.date,
|
date: <Date>data.date,
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataGatheringService.gatherProfileData([
|
|
||||||
{
|
|
||||||
dataSource: data.dataSource,
|
|
||||||
symbol: data.SymbolProfile.connectOrCreate.create.symbol
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
await this.cacheService.flush();
|
await this.cacheService.flush();
|
||||||
|
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
|
delete data.currency;
|
||||||
|
delete data.dataSource;
|
||||||
|
delete data.symbol;
|
||||||
delete data.userId;
|
delete data.userId;
|
||||||
|
|
||||||
const orderData: Prisma.OrderCreateInput = data;
|
const orderData: Prisma.OrderCreateInput = data;
|
||||||
@ -193,50 +200,60 @@ export class OrderService {
|
|||||||
value,
|
value,
|
||||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
value,
|
value,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOrder(params: {
|
public async updateOrder({
|
||||||
|
data,
|
||||||
|
where
|
||||||
|
}: {
|
||||||
|
data: Prisma.OrderUpdateInput & {
|
||||||
|
currency?: string;
|
||||||
|
dataSource?: DataSource;
|
||||||
|
symbol?: string;
|
||||||
|
};
|
||||||
where: Prisma.OrderWhereUniqueInput;
|
where: Prisma.OrderWhereUniqueInput;
|
||||||
data: Prisma.OrderUpdateInput;
|
|
||||||
}): Promise<Order> {
|
}): Promise<Order> {
|
||||||
const { data, where } = params;
|
|
||||||
|
|
||||||
if (data.Account.connect.id_userId.id === null) {
|
if (data.Account.connect.id_userId.id === null) {
|
||||||
delete data.Account;
|
delete data.Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isDraft = false;
|
||||||
|
|
||||||
if (data.type === 'ITEM') {
|
if (data.type === 'ITEM') {
|
||||||
const name = data.symbol;
|
const name = data.SymbolProfile.connect.dataSource_symbol.symbol;
|
||||||
|
|
||||||
data.symbol = null;
|
|
||||||
data.SymbolProfile = { update: { name } };
|
data.SymbolProfile = { update: { name } };
|
||||||
}
|
} else {
|
||||||
|
isDraft = isAfter(data.date as Date, endOfToday());
|
||||||
|
|
||||||
const isDraft = isAfter(data.date as Date, endOfToday());
|
if (!isDraft) {
|
||||||
|
// Gather symbol data of order in the background, if not draft
|
||||||
if (!isDraft) {
|
this.dataGatheringService.gatherSymbols([
|
||||||
// Gather symbol data of order in the background, if not draft
|
{
|
||||||
this.dataGatheringService.gatherSymbols([
|
dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource,
|
||||||
{
|
date: <Date>data.date,
|
||||||
dataSource: <DataSource>data.dataSource,
|
symbol: data.SymbolProfile.connect.dataSource_symbol.symbol
|
||||||
date: <Date>data.date,
|
}
|
||||||
symbol: <string>data.symbol
|
]);
|
||||||
}
|
}
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.cacheService.flush();
|
await this.cacheService.flush();
|
||||||
|
|
||||||
|
delete data.currency;
|
||||||
|
delete data.dataSource;
|
||||||
|
delete data.symbol;
|
||||||
|
|
||||||
return this.prismaService.order.update({
|
return this.prismaService.order.update({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface CurrentPositions {
|
export interface CurrentPositions extends ResponseError {
|
||||||
hasErrors: boolean;
|
|
||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
@ -66,6 +66,7 @@ describe('PortfolioCalculatorNew', () => {
|
|||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('0'),
|
currentValue: new Big('0'),
|
||||||
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
|
@ -55,6 +55,7 @@ describe('PortfolioCalculatorNew', () => {
|
|||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('297.8'),
|
currentValue: new Big('297.8'),
|
||||||
|
errors: [],
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
ResponseError,
|
||||||
|
TimelinePosition,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Type as TypeOfOrder } from '@prisma/client';
|
import { Type as TypeOfOrder } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -232,6 +236,8 @@ export class PortfolioCalculatorNew {
|
|||||||
const positions: TimelinePosition[] = [];
|
const positions: TimelinePosition[] = [];
|
||||||
let hasAnySymbolMetricsErrors = false;
|
let hasAnySymbolMetricsErrors = false;
|
||||||
|
|
||||||
|
const errors: ResponseError['errors'] = [];
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
for (const item of lastTransactionPoint.items) {
|
||||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
||||||
|
|
||||||
@ -272,12 +278,17 @@ export class PortfolioCalculatorNew {
|
|||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const overall = this.calculateOverallPerformance(positions, initialValues);
|
const overall = this.calculateOverallPerformance(positions, initialValues);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overall,
|
...overall,
|
||||||
|
errors,
|
||||||
positions,
|
positions,
|
||||||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary
|
PortfolioSummary
|
||||||
@ -204,10 +204,11 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
@Query('range') range
|
@Query('range') range
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const performanceInformation = await this.portfolioServiceStrategy
|
const performanceInformation = await this.portfolioServiceStrategy
|
||||||
.get()
|
.get()
|
||||||
.getPerformance(impersonationId, range);
|
.getPerformance(impersonationId, range);
|
||||||
|
@ -24,7 +24,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
@ -100,15 +100,22 @@ export class PortfolioServiceNew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const value = details.accounts[account.id]?.current ?? 0;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
...account,
|
...account,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
convertedBalance: this.exchangeRateDataService.toCurrency(
|
value,
|
||||||
|
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
account.balance,
|
account.balance,
|
||||||
account.currency,
|
account.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
value: details.accounts[account.id]?.current ?? 0
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
|
value,
|
||||||
|
account.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
delete result.Order;
|
delete result.Order;
|
||||||
@ -119,17 +126,26 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
||||||
const accounts = await this.getAccounts(aUserId);
|
const accounts = await this.getAccounts(aUserId);
|
||||||
let totalBalance = 0;
|
let totalBalanceInBaseCurrency = new Big(0);
|
||||||
let totalValue = 0;
|
let totalValueInBaseCurrency = new Big(0);
|
||||||
let transactionCount = 0;
|
let transactionCount = 0;
|
||||||
|
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
totalBalance += account.convertedBalance;
|
totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
|
||||||
totalValue += account.value;
|
account.balanceInBaseCurrency
|
||||||
|
);
|
||||||
|
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
|
||||||
|
account.valueInBaseCurrency
|
||||||
|
);
|
||||||
transactionCount += account.transactionCount;
|
transactionCount += account.transactionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { accounts, totalBalance, totalValue, transactionCount };
|
return {
|
||||||
|
accounts,
|
||||||
|
transactionCount,
|
||||||
|
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
|
||||||
|
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
@ -293,13 +309,11 @@ export class PortfolioServiceNew {
|
|||||||
orders: portfolioOrders
|
orders: portfolioOrders
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
|
||||||
return { accounts: {}, holdings: {}, hasErrors: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(
|
||||||
|
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||||
|
);
|
||||||
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
startDate
|
startDate
|
||||||
@ -312,9 +326,11 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
const totalInvestment = currentPositions.totalInvestment.plus(
|
const totalInvestment = currentPositions.totalInvestment.plus(
|
||||||
cashDetails.balance
|
cashDetails.balanceInBaseCurrency
|
||||||
|
);
|
||||||
|
const totalValue = currentPositions.currentValue.plus(
|
||||||
|
cashDetails.balanceInBaseCurrency
|
||||||
);
|
);
|
||||||
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map((position) => {
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
||||||
return {
|
return {
|
||||||
@ -434,7 +450,7 @@ export class PortfolioServiceNew {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].SymbolProfile.currency;
|
||||||
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||||
aSymbol
|
aSymbol
|
||||||
]);
|
]);
|
||||||
@ -444,13 +460,13 @@ export class PortfolioServiceNew {
|
|||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
})
|
})
|
||||||
.map((order) => ({
|
.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
dataSource: order.SymbolProfile.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(order.fee),
|
fee: new Big(order.fee),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(order.unitPrice)
|
unitPrice: new Big(order.unitPrice)
|
||||||
}));
|
}));
|
||||||
@ -714,7 +730,7 @@ export class PortfolioServiceNew {
|
|||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
@ -760,6 +776,7 @@ export class PortfolioServiceNew {
|
|||||||
currentPositions.netPerformancePercentage.toNumber();
|
currentPositions.netPerformancePercentage.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
errors: currentPositions.errors,
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
@ -869,7 +886,7 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const performanceInformation = await this.getPerformance(aImpersonationId);
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
const { balance } = await this.accountService.getCashDetails(
|
const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
|
||||||
userId,
|
userId,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
@ -887,7 +904,7 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
|
|
||||||
const netWorth = new Big(balance)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
.plus(performanceInformation.performance.currentValue)
|
.plus(performanceInformation.performance.currentValue)
|
||||||
.plus(items)
|
.plus(items)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
@ -917,7 +934,7 @@ export class PortfolioServiceNew {
|
|||||||
netWorth,
|
netWorth,
|
||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
cash: balance,
|
cash: balanceInBaseCurrency,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
ordersCount: orders.filter((order) => {
|
ordersCount: orders.filter((order) => {
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
@ -1006,7 +1023,7 @@ export class PortfolioServiceNew {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -1025,7 +1042,7 @@ export class PortfolioServiceNew {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -1047,7 +1064,7 @@ export class PortfolioServiceNew {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -1100,24 +1117,24 @@ export class PortfolioServiceNew {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
dataSource: order.SymbolProfile.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(
|
fee: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(
|
unitPrice: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
order.unitPrice,
|
order.unitPrice,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1153,22 +1170,18 @@ export class PortfolioServiceNew {
|
|||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
|
||||||
account.balance,
|
|
||||||
account.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
accounts[account.id] = {
|
accounts[account.id] = {
|
||||||
balance: convertedBalance,
|
balance: account.balance,
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
current: convertedBalance,
|
current: account.balance,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
original: convertedBalance
|
original: account.balance
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const order of ordersByAccount) {
|
for (const order of ordersByAccount) {
|
||||||
let currentValueOfSymbol =
|
let currentValueOfSymbol =
|
||||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice;
|
order.quantity *
|
||||||
|
portfolioItemsNow[order.SymbolProfile.symbol].marketPrice;
|
||||||
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
||||||
|
|
||||||
if (order.type === 'SELL') {
|
if (order.type === 'SELL') {
|
||||||
@ -1218,7 +1231,7 @@ export class PortfolioServiceNew {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.quantity * order.unitPrice,
|
order.quantity * order.unitPrice,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
currency
|
currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -25,7 +25,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
@ -99,15 +99,22 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const value = details.accounts[account.id]?.current ?? 0;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
...account,
|
...account,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
convertedBalance: this.exchangeRateDataService.toCurrency(
|
value,
|
||||||
|
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
account.balance,
|
account.balance,
|
||||||
account.currency,
|
account.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
),
|
),
|
||||||
value: details.accounts[account.id]?.current ?? 0
|
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||||
|
value,
|
||||||
|
account.currency,
|
||||||
|
userCurrency
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
delete result.Order;
|
delete result.Order;
|
||||||
@ -118,17 +125,26 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
||||||
const accounts = await this.getAccounts(aUserId);
|
const accounts = await this.getAccounts(aUserId);
|
||||||
let totalBalance = 0;
|
let totalBalanceInBaseCurrency = new Big(0);
|
||||||
let totalValue = 0;
|
let totalValueInBaseCurrency = new Big(0);
|
||||||
let transactionCount = 0;
|
let transactionCount = 0;
|
||||||
|
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
totalBalance += account.convertedBalance;
|
totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
|
||||||
totalValue += account.value;
|
account.balanceInBaseCurrency
|
||||||
|
);
|
||||||
|
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
|
||||||
|
account.valueInBaseCurrency
|
||||||
|
);
|
||||||
transactionCount += account.transactionCount;
|
transactionCount += account.transactionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { accounts, totalBalance, totalValue, transactionCount };
|
return {
|
||||||
|
accounts,
|
||||||
|
transactionCount,
|
||||||
|
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
|
||||||
|
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
@ -281,13 +297,11 @@ export class PortfolioService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
|
||||||
return { accounts: {}, holdings: {}, hasErrors: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const portfolioStart = parseDate(
|
||||||
|
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
||||||
|
);
|
||||||
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
startDate
|
startDate
|
||||||
@ -300,9 +314,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const holdings: PortfolioDetails['holdings'] = {};
|
const holdings: PortfolioDetails['holdings'] = {};
|
||||||
const totalInvestment = currentPositions.totalInvestment.plus(
|
const totalInvestment = currentPositions.totalInvestment.plus(
|
||||||
cashDetails.balance
|
cashDetails.balanceInBaseCurrency
|
||||||
|
);
|
||||||
|
const totalValue = currentPositions.currentValue.plus(
|
||||||
|
cashDetails.balanceInBaseCurrency
|
||||||
);
|
);
|
||||||
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
|
||||||
|
|
||||||
const dataGatheringItems = currentPositions.positions.map((position) => {
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
||||||
return {
|
return {
|
||||||
@ -422,7 +438,7 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].SymbolProfile.currency;
|
||||||
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||||
aSymbol
|
aSymbol
|
||||||
]);
|
]);
|
||||||
@ -432,13 +448,13 @@ export class PortfolioService {
|
|||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
})
|
})
|
||||||
.map((order) => ({
|
.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
dataSource: order.SymbolProfile.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(order.fee),
|
fee: new Big(order.fee),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(order.unitPrice)
|
unitPrice: new Big(order.unitPrice)
|
||||||
}));
|
}));
|
||||||
@ -696,7 +712,7 @@ export class PortfolioService {
|
|||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
@ -848,7 +864,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const performanceInformation = await this.getPerformance(aImpersonationId);
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
const { balance } = await this.accountService.getCashDetails(
|
const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
|
||||||
userId,
|
userId,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
@ -866,7 +882,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
|
|
||||||
const netWorth = new Big(balance)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
.plus(performanceInformation.performance.currentValue)
|
.plus(performanceInformation.performance.currentValue)
|
||||||
.plus(items)
|
.plus(items)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
@ -882,7 +898,7 @@ export class PortfolioService {
|
|||||||
totalSell,
|
totalSell,
|
||||||
annualizedPerformancePercent:
|
annualizedPerformancePercent:
|
||||||
performanceInformation.performance.annualizedPerformancePercent,
|
performanceInformation.performance.annualizedPerformancePercent,
|
||||||
cash: balance,
|
cash: balanceInBaseCurrency,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
ordersCount: orders.filter((order) => {
|
ordersCount: orders.filter((order) => {
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
@ -971,7 +987,7 @@ export class PortfolioService {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -990,7 +1006,7 @@ export class PortfolioService {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -1012,7 +1028,7 @@ export class PortfolioService {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -1064,24 +1080,24 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
currency: order.currency,
|
currency: order.SymbolProfile.currency,
|
||||||
dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
|
dataSource: order.SymbolProfile.dataSource,
|
||||||
date: format(order.date, DATE_FORMAT),
|
date: format(order.date, DATE_FORMAT),
|
||||||
fee: new Big(
|
fee: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
order.fee,
|
order.fee,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
name: order.SymbolProfile?.name,
|
name: order.SymbolProfile?.name,
|
||||||
quantity: new Big(order.quantity),
|
quantity: new Big(order.quantity),
|
||||||
symbol: order.symbol,
|
symbol: order.SymbolProfile.symbol,
|
||||||
type: order.type,
|
type: order.type,
|
||||||
unitPrice: new Big(
|
unitPrice: new Big(
|
||||||
this.exchangeRateDataService.toCurrency(
|
this.exchangeRateDataService.toCurrency(
|
||||||
order.unitPrice,
|
order.unitPrice,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1113,22 +1129,18 @@ export class PortfolioService {
|
|||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
|
||||||
account.balance,
|
|
||||||
account.currency,
|
|
||||||
userCurrency
|
|
||||||
);
|
|
||||||
accounts[account.id] = {
|
accounts[account.id] = {
|
||||||
balance: convertedBalance,
|
balance: account.balance,
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
current: convertedBalance,
|
current: account.balance,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
original: convertedBalance
|
original: account.balance
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const order of ordersByAccount) {
|
for (const order of ordersByAccount) {
|
||||||
let currentValueOfSymbol =
|
let currentValueOfSymbol =
|
||||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice;
|
order.quantity *
|
||||||
|
portfolioItemsNow[order.SymbolProfile.symbol].marketPrice;
|
||||||
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
||||||
|
|
||||||
if (order.type === 'SELL') {
|
if (order.type === 'SELL') {
|
||||||
@ -1178,7 +1190,7 @@ export class PortfolioService {
|
|||||||
.map((order) => {
|
.map((order) => {
|
||||||
return this.exchangeRateDataService.toCurrency(
|
return this.exchangeRateDataService.toCurrency(
|
||||||
order.quantity * order.unitPrice,
|
order.quantity * order.unitPrice,
|
||||||
order.currency,
|
order.SymbolProfile.currency,
|
||||||
currency
|
currency
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,6 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
activity.SymbolProfile.dataSource = encodeDataSource(
|
activity.SymbolProfile.dataSource = encodeDataSource(
|
||||||
activity.SymbolProfile.dataSource
|
activity.SymbolProfile.dataSource
|
||||||
);
|
);
|
||||||
activity.dataSource = encodeDataSource(activity.dataSource);
|
|
||||||
return activity;
|
return activity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -41,6 +40,14 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
data.dataSource = encodeDataSource(data.dataSource);
|
data.dataSource = encodeDataSource(data.dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.errors) {
|
||||||
|
for (const error of data.errors) {
|
||||||
|
if (error.dataSource) {
|
||||||
|
error.dataSource = encodeDataSource(error.dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.holdings) {
|
if (data.holdings) {
|
||||||
for (const symbol of Object.keys(data.holdings)) {
|
for (const symbol of Object.keys(data.holdings)) {
|
||||||
if (data.holdings[symbol].dataSource) {
|
if (data.holdings[symbol].dataSource) {
|
||||||
@ -58,13 +65,6 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.orders) {
|
|
||||||
data.orders.map((order) => {
|
|
||||||
order.dataSource = encodeDataSource(order.dataSource);
|
|
||||||
return order;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.positions) {
|
if (data.positions) {
|
||||||
data.positions.map((position) => {
|
data.positions.map((position) => {
|
||||||
position.dataSource = encodeDataSource(position.dataSource);
|
position.dataSource = encodeDataSource(position.dataSource);
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
PROPERTY_LOCKED_DATA_GATHERING
|
PROPERTY_LOCKED_DATA_GATHERING
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
@ -121,13 +122,7 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async gatherSymbol({
|
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
@ -554,19 +549,24 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
||||||
const distinctOrders = await this.prismaService.order.findMany({
|
const symbolProfiles = await this.prismaService.symbolProfile.findMany({
|
||||||
distinct: ['symbol'],
|
orderBy: [{ symbol: 'asc' }]
|
||||||
orderBy: [{ symbol: 'asc' }],
|
|
||||||
select: { dataSource: true, symbol: true }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return distinctOrders.filter((distinctOrder) => {
|
return symbolProfiles
|
||||||
return (
|
.filter((symbolProfile) => {
|
||||||
distinctOrder.dataSource !== DataSource.GHOSTFOLIO &&
|
return (
|
||||||
distinctOrder.dataSource !== DataSource.MANUAL &&
|
symbolProfile.dataSource !== DataSource.GHOSTFOLIO &&
|
||||||
distinctOrder.dataSource !== DataSource.RAKUTEN
|
symbolProfile.dataSource !== DataSource.MANUAL &&
|
||||||
);
|
symbolProfile.dataSource !== DataSource.RAKUTEN
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
.map((symbolProfile) => {
|
||||||
|
return {
|
||||||
|
dataSource: symbolProfile.dataSource,
|
||||||
|
symbol: symbolProfile.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isDataGatheringNeeded() {
|
private async isDataGatheringNeeded() {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
@ -10,7 +11,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import { isAfter, isBefore, parse } from 'date-fns';
|
import { isAfter, isBefore, parse } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
|
||||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -7,11 +7,7 @@ import {
|
|||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import {
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
getYesterday,
|
|
||||||
isGhostfolioScraperApiSymbol
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
@ -29,7 +25,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public canHandle(symbol: string) {
|
public canHandle(symbol: string) {
|
||||||
return isGhostfolioScraperApiSymbol(symbol);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAssetProfile(
|
public async getAssetProfile(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
@ -14,8 +15,6 @@ import { DataSource, SymbolProfile } from '@prisma/client';
|
|||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RakutenRapidApiService implements DataProviderInterface {
|
export class RakutenRapidApiService implements DataProviderInterface {
|
||||||
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
|
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
@ -19,9 +20,7 @@ import * as bent from 'bent';
|
|||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { countries } from 'countries-list';
|
import { countries } from 'countries-list';
|
||||||
import { addDays, format, isSameDay } from 'date-fns';
|
import { addDays, format, isSameDay } from 'date-fns';
|
||||||
import yahooFinance2 from 'yahoo-finance2';
|
import yahooFinance from 'yahoo-finance2';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YahooFinanceService implements DataProviderInterface {
|
export class YahooFinanceService implements DataProviderInterface {
|
||||||
@ -80,7 +79,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
const assetProfile = await yahooFinance2.quoteSummary(symbol, {
|
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||||
modules: ['price', 'summaryProfile']
|
modules: ['price', 'summaryProfile']
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,7 +142,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol);
|
const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalResult = await yahooFinance2.historical(
|
const historicalResult = await yahooFinance.historical(
|
||||||
yahooFinanceSymbol,
|
yahooFinanceSymbol,
|
||||||
{
|
{
|
||||||
interval: '1d',
|
interval: '1d',
|
||||||
@ -202,7 +201,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
|
||||||
const quotes = await yahooFinance2.quote(yahooFinanceSymbols);
|
const quotes = await yahooFinance.quote(yahooFinanceSymbols);
|
||||||
|
|
||||||
for (const quote of quotes) {
|
for (const quote of quotes) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
|
@ -114,6 +114,10 @@ export class ExchangeRateDataService {
|
|||||||
aFromCurrency: string,
|
aFromCurrency: string,
|
||||||
aToCurrency: string
|
aToCurrency: string
|
||||||
) {
|
) {
|
||||||
|
if (aValue === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
|
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
|
||||||
return isNaN(exchangeRate);
|
return isNaN(exchangeRate);
|
||||||
});
|
});
|
||||||
@ -187,12 +191,7 @@ export class ExchangeRateDataService {
|
|||||||
await this.prismaService.symbolProfile.findMany({
|
await this.prismaService.symbolProfile.findMany({
|
||||||
distinct: ['currency'],
|
distinct: ['currency'],
|
||||||
orderBy: [{ currency: 'asc' }],
|
orderBy: [{ currency: 'asc' }],
|
||||||
select: { currency: true },
|
select: { currency: true }
|
||||||
where: {
|
|
||||||
currency: {
|
|
||||||
not: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
).forEach((symbolProfile) => {
|
).forEach((symbolProfile) => {
|
||||||
currencies.push(symbolProfile.currency);
|
currencies.push(symbolProfile.currency);
|
||||||
@ -206,7 +205,7 @@ export class ExchangeRateDataService {
|
|||||||
currencies = currencies.concat(customCurrencies);
|
currencies = currencies.concat(customCurrencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniq(currencies).sort();
|
return uniq(currencies).filter(Boolean).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareCurrencyPairs(aCurrencies: string[]) {
|
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||||
|
@ -2,6 +2,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat
|
|||||||
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
|
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';
|
||||||
|
|
||||||
@ -9,13 +10,7 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
|
|||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async deleteMany({
|
public async deleteMany({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
return this.prismaService.marketData.deleteMany({
|
return this.prismaService.marketData.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -86,7 +86,7 @@
|
|||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="element.convertedBalance"
|
[value]="element.balance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||||
@ -94,7 +94,7 @@
|
|||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="totalBalance"
|
[value]="totalBalanceInBaseCurrency"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -116,7 +116,7 @@
|
|||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="totalValue"
|
[value]="totalValueInBaseCurrency"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -24,8 +24,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() totalBalance: number;
|
@Input() totalBalanceInBaseCurrency: number;
|
||||||
@Input() totalValue: number;
|
@Input() totalValueInBaseCurrency: number;
|
||||||
@Input() transactionCount: number;
|
@Input() transactionCount: number;
|
||||||
|
|
||||||
@Output() accountDeleted = new EventEmitter<string>();
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -44,39 +45,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
this.fetchAdminMarketData();
|
this.fetchAdminMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteProfileData({
|
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.deleteProfileData({ dataSource, symbol })
|
.deleteProfileData({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGatherProfileDataBySymbol({
|
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.gatherProfileDataBySymbol({ dataSource, symbol })
|
.gatherProfileDataBySymbol({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGatherSymbol({
|
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.gatherSymbol({ dataSource, symbol })
|
.gatherSymbol({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -93,13 +76,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCurrentProfile({
|
public setCurrentProfile({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.marketDataDetails = [];
|
this.marketDataDetails = [];
|
||||||
|
|
||||||
if (this.currentSymbol === symbol) {
|
if (this.currentSymbol === symbol) {
|
||||||
@ -129,13 +106,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchAdminMarketDataBySymbol({
|
private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
@ -7,7 +7,11 @@ import {
|
|||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioPerformance,
|
||||||
|
UniqueAsset,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
@ -24,6 +28,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions = defaultDateRangeOptions;
|
public dateRangeOptions = defaultDateRangeOptions;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public errors: UniqueAsset[];
|
||||||
public hasError: boolean;
|
public hasError: boolean;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
@ -126,6 +131,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
|
this.errors = response.errors;
|
||||||
this.hasError = response.hasErrors;
|
this.hasError = response.hasErrors;
|
||||||
this.performance = response.performance;
|
this.performance = response.performance;
|
||||||
this.isLoadingPerformance = false;
|
this.isLoadingPerformance = false;
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[errors]="errors"
|
||||||
[hasError]="hasError"
|
[hasError]="hasError"
|
||||||
[isAllTimeHigh]="isAllTimeHigh"
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
[isAllTimeLow]="isAllTimeLow"
|
[isAllTimeLow]="isAllTimeLow"
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
|
(click)="errors?.length > 0 && onShowErrors()"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="hasError && !isLoading"
|
*ngIf="hasError && !isLoading"
|
||||||
|
@ -7,7 +7,10 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioPerformance,
|
||||||
|
ResponseError
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { CountUp } from 'countup.js';
|
import { CountUp } from 'countup.js';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@ -20,6 +23,7 @@ import { isNumber } from 'lodash';
|
|||||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() errors: ResponseError['errors'];
|
||||||
@Input() hasError: boolean;
|
@Input() hasError: boolean;
|
||||||
@Input() isAllTimeHigh: boolean;
|
@Input() isAllTimeHigh: boolean;
|
||||||
@Input() isAllTimeLow: boolean;
|
@Input() isAllTimeLow: boolean;
|
||||||
@ -69,4 +73,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onShowErrors() {
|
||||||
|
const errorMessageParts = this.errors.map((error) => {
|
||||||
|
return `${error.symbol} (${error.dataSource})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(errorMessageParts.join('\n'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client';
|
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -75,13 +75,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}): void {
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||||
});
|
});
|
||||||
|
@ -28,8 +28,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToCreateAccount: boolean;
|
public hasPermissionToCreateAccount: boolean;
|
||||||
public hasPermissionToDeleteAccount: boolean;
|
public hasPermissionToDeleteAccount: boolean;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public totalBalance = 0;
|
public totalBalanceInBaseCurrency = 0;
|
||||||
public totalValue = 0;
|
public totalValueInBaseCurrency = 0;
|
||||||
public transactionCount = 0;
|
public transactionCount = 0;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -106,18 +106,25 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.dataService
|
this.dataService
|
||||||
.fetchAccounts()
|
.fetchAccounts()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => {
|
.subscribe(
|
||||||
this.accounts = accounts;
|
({
|
||||||
this.totalBalance = totalBalance;
|
accounts,
|
||||||
this.totalValue = totalValue;
|
totalBalanceInBaseCurrency,
|
||||||
this.transactionCount = transactionCount;
|
totalValueInBaseCurrency,
|
||||||
|
transactionCount
|
||||||
|
}) => {
|
||||||
|
this.accounts = accounts;
|
||||||
|
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
|
||||||
|
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
|
||||||
if (this.accounts?.length <= 0) {
|
if (this.accounts?.length <= 0) {
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteAccount(aId: string) {
|
public onDeleteAccount(aId: string) {
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
|
||||||
[totalBalance]="totalBalance"
|
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
|
||||||
[totalValue]="totalValue"
|
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
|
||||||
[transactionCount]="transactionCount"
|
[transactionCount]="transactionCount"
|
||||||
(accountDeleted)="onDeleteAccount($event)"
|
(accountDeleted)="onDeleteAccount($event)"
|
||||||
(accountToUpdate)="onUpdateAccount($event)"
|
(accountToUpdate)="onUpdateAccount($event)"
|
||||||
|
@ -10,6 +10,7 @@ import { prettifySymbol } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -64,7 +65,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public symbols: {
|
public symbols: {
|
||||||
[name: string]: { name: string; symbol: string; value: number };
|
[name: string]: {
|
||||||
|
dataSource?: DataSource;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -281,6 +287,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
if (position.assetClass === AssetClass.EQUITY) {
|
if (position.assetClass === AssetClass.EQUITY) {
|
||||||
this.symbols[prettifySymbol(symbol)] = {
|
this.symbols[prettifySymbol(symbol)] = {
|
||||||
|
dataSource: position.dataSource,
|
||||||
name: position.name,
|
name: position.name,
|
||||||
symbol: prettifySymbol(symbol),
|
symbol: prettifySymbol(symbol),
|
||||||
value: aPeriod === 'original' ? position.investment : position.value
|
value: aPeriod === 'original' ? position.investment : position.value
|
||||||
@ -295,6 +302,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.initializeAnalysisData(this.period);
|
this.initializeAnalysisData(this.period);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||||
|
if (dataSource && symbol) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -89,12 +89,14 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
|
cursor="pointer"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['symbol']"
|
[keys]="['symbol']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="symbols"
|
[positions]="symbols"
|
||||||
[showLabels]="deviceType !== 'mobile'"
|
[showLabels]="deviceType !== 'mobile'"
|
||||||
|
(proportionChartClicked)="onProportionChartClicked($event)"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -158,11 +158,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
this.activityForm.controls['type'].disable();
|
this.activityForm.controls['type'].disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data.activity?.symbol) {
|
if (this.data.activity?.SymbolProfile?.symbol) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem({
|
.fetchSymbolItem({
|
||||||
dataSource: this.data.activity?.dataSource,
|
dataSource: this.data.activity?.SymbolProfile?.dataSource,
|
||||||
symbol: this.data.activity?.symbol
|
symbol: this.data.activity?.SymbolProfile?.symbol
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketPrice }) => {
|
.subscribe(({ marketPrice }) => {
|
||||||
@ -196,9 +196,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
|
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
|
||||||
|
|
||||||
this.data.activity.currency = null;
|
this.data.activity.SymbolProfile = null;
|
||||||
this.data.activity.dataSource = null;
|
|
||||||
this.data.activity.symbol = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
@ -259,9 +257,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.data.activity.currency = null;
|
this.data.activity.SymbolProfile = null;
|
||||||
this.data.activity.dataSource = null;
|
|
||||||
this.data.activity.unitPrice = null;
|
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.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 { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
AdminMarketDataDetails,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
@ -14,13 +17,7 @@ import { Observable, map } from 'rxjs';
|
|||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
public deleteProfileData({
|
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
return this.http.delete<void>(
|
return this.http.delete<void>(
|
||||||
`/api/admin/profile-data/${dataSource}/${symbol}`
|
`/api/admin/profile-data/${dataSource}/${symbol}`
|
||||||
);
|
);
|
||||||
@ -53,13 +50,7 @@ export class AdminService {
|
|||||||
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public gatherProfileDataBySymbol({
|
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
return this.http.post<void>(
|
return this.http.post<void>(
|
||||||
`/api/admin/gather/profile-data/${dataSource}/${symbol}`,
|
`/api/admin/gather/profile-data/${dataSource}/${symbol}`,
|
||||||
{}
|
{}
|
||||||
@ -70,10 +61,8 @@ export class AdminService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: UniqueAsset & {
|
||||||
dataSource: DataSource;
|
|
||||||
date?: Date;
|
date?: Date;
|
||||||
symbol: string;
|
|
||||||
}) {
|
}) {
|
||||||
let url = `/api/admin/gather/${dataSource}/${symbol}`;
|
let url = `/api/admin/gather/${dataSource}/${symbol}`;
|
||||||
|
|
||||||
|
@ -24,9 +24,11 @@ import {
|
|||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -188,13 +190,13 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
|
public fetchPortfolioPerformance(params: { [param: string]: any }) {
|
||||||
return this.http.get<{
|
return this.http.get<PortfolioPerformanceResponse>(
|
||||||
hasErrors: boolean;
|
'/api/portfolio/performance',
|
||||||
performance: PortfolioPerformance;
|
{
|
||||||
}>('/api/portfolio/performance', {
|
params
|
||||||
params: aParams
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioPublic(aId: string) {
|
public fetchPortfolioPublic(aId: string) {
|
||||||
|
@ -102,10 +102,6 @@ export function isCurrency(aSymbol = '') {
|
|||||||
return currencies[aSymbol];
|
return currencies[aSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGhostfolioScraperApiSymbol(aSymbol = '') {
|
|
||||||
return aSymbol.startsWith(ghostfolioScraperApiSymbolPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetHours(aDate: Date) {
|
export function resetHours(aDate: Date) {
|
||||||
const year = getYear(aDate);
|
const year = getYear(aDate);
|
||||||
const month = getMonth(aDate);
|
const month = getMonth(aDate);
|
||||||
|
@ -2,7 +2,7 @@ import { AccountWithValue } from '@ghostfolio/common/types';
|
|||||||
|
|
||||||
export interface Accounts {
|
export interface Accounts {
|
||||||
accounts: AccountWithValue[];
|
accounts: AccountWithValue[];
|
||||||
totalBalance: number;
|
totalBalanceInBaseCurrency: number;
|
||||||
totalValue: number;
|
totalValueInBaseCurrency: number;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Property } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface AdminData {
|
export interface AdminData {
|
||||||
dataGatheringProgress?: number;
|
dataGatheringProgress?: number;
|
||||||
exchangeRates: { label1: string; label2: string; value: number }[];
|
exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
|
@ -21,7 +21,10 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
|||||||
import { PortfolioReport } from './portfolio-report.interface';
|
import { PortfolioReport } from './portfolio-report.interface';
|
||||||
import { PortfolioSummary } from './portfolio-summary.interface';
|
import { PortfolioSummary } from './portfolio-summary.interface';
|
||||||
import { Position } from './position.interface';
|
import { Position } from './position.interface';
|
||||||
|
import { ResponseError } from './responses/errors.interface';
|
||||||
|
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||||
import { TimelinePosition } from './timeline-position.interface';
|
import { TimelinePosition } from './timeline-position.interface';
|
||||||
|
import { UniqueAsset } from './unique-asset.interface';
|
||||||
import { UserSettings } from './user-settings.interface';
|
import { UserSettings } from './user-settings.interface';
|
||||||
import { UserWithSettings } from './user-with-settings';
|
import { UserWithSettings } from './user-with-settings';
|
||||||
import { User } from './user.interface';
|
import { User } from './user.interface';
|
||||||
@ -42,13 +45,16 @@ export {
|
|||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioReportRule,
|
PortfolioReportRule,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
|
ResponseError,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
|
UniqueAsset,
|
||||||
User,
|
User,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
UserWithSettings
|
UserWithSettings
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { UniqueAsset } from '../unique-asset.interface';
|
||||||
|
|
||||||
|
export interface ResponseError {
|
||||||
|
errors?: UniqueAsset[];
|
||||||
|
hasErrors: boolean;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { PortfolioPerformance } from '../portfolio-performance.interface';
|
||||||
|
import { ResponseError } from './errors.interface';
|
||||||
|
|
||||||
|
export interface PortfolioPerformanceResponse extends ResponseError {
|
||||||
|
performance: PortfolioPerformance;
|
||||||
|
}
|
6
libs/common/src/lib/interfaces/unique-asset.interface.ts
Normal file
6
libs/common/src/lib/interfaces/unique-asset.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface UniqueAsset {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
|
||||||
export type AccountWithValue = AccountModel & {
|
export type AccountWithValue = AccountModel & {
|
||||||
convertedBalance: number;
|
balanceInBaseCurrency: number;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueInBaseCurrency: number;
|
||||||
};
|
};
|
||||||
|
@ -144,7 +144,7 @@
|
|||||||
class="d-none d-lg-table-cell px-1"
|
class="d-none d-lg-table-cell px-1"
|
||||||
mat-cell
|
mat-cell
|
||||||
>
|
>
|
||||||
{{ element.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 }}
|
||||||
@ -362,7 +362,7 @@
|
|||||||
!row.isDraft &&
|
!row.isDraft &&
|
||||||
row.type !== 'ITEM' &&
|
row.type !== 'ITEM' &&
|
||||||
onOpenPositionDialog({
|
onOpenPositionDialog({
|
||||||
dataSource: row.dataSource,
|
dataSource: row.SymbolProfile.dataSource,
|
||||||
symbol: row.SymbolProfile.symbol
|
symbol: row.SymbolProfile.symbol
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@ -21,6 +21,7 @@ import { MatTableDataSource } from '@angular/material/table';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -199,13 +200,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
this.import.emit();
|
this.import.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}): void {
|
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||||
});
|
});
|
||||||
|
@ -3,14 +3,17 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { getTextColor } from '@ghostfolio/common/helper';
|
import { getTextColor } from '@ghostfolio/common/helper';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { Tooltip } from 'chart.js';
|
import { Tooltip } from 'chart.js';
|
||||||
import { LinearScale } from 'chart.js';
|
import { LinearScale } from 'chart.js';
|
||||||
@ -30,6 +33,7 @@ export class PortfolioProportionChartComponent
|
|||||||
implements AfterViewInit, OnChanges, OnDestroy
|
implements AfterViewInit, OnChanges, OnDestroy
|
||||||
{
|
{
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
|
@Input() cursor: string;
|
||||||
@Input() isInPercent = false;
|
@Input() isInPercent = false;
|
||||||
@Input() keys: string[] = [];
|
@Input() keys: string[] = [];
|
||||||
@Input() locale = '';
|
@Input() locale = '';
|
||||||
@ -37,11 +41,14 @@ export class PortfolioProportionChartComponent
|
|||||||
@Input() showLabels = false;
|
@Input() showLabels = false;
|
||||||
@Input() positions: {
|
@Input() positions: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'type'> & {
|
[symbol: string]: Pick<PortfolioPosition, 'type'> & {
|
||||||
|
dataSource?: DataSource;
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
@Output() proportionChartClicked = new EventEmitter<UniqueAsset>();
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
public chart: Chart;
|
public chart: Chart;
|
||||||
@ -256,6 +263,21 @@ export class PortfolioProportionChartComponent
|
|||||||
layout: {
|
layout: {
|
||||||
padding: this.showLabels === true ? 100 : 0
|
padding: this.showLabels === true ? 100 : 0
|
||||||
},
|
},
|
||||||
|
onClick: (event, activeElements) => {
|
||||||
|
const dataIndex = activeElements[0].index;
|
||||||
|
const symbol: string = event.chart.data.labels[dataIndex];
|
||||||
|
|
||||||
|
const dataSource = this.positions[symbol]?.dataSource;
|
||||||
|
|
||||||
|
this.proportionChartClicked.emit({ dataSource, symbol });
|
||||||
|
},
|
||||||
|
onHover: (event, chartElement) => {
|
||||||
|
if (this.cursor) {
|
||||||
|
event.native.target.style.cursor = chartElement[0]
|
||||||
|
? this.cursor
|
||||||
|
: 'default';
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
datalabels: {
|
datalabels: {
|
||||||
color: (context) => {
|
color: (context) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.121.0",
|
"version": "1.123.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" DROP COLUMN "dataSource";
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" DROP COLUMN "currency";
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" DROP COLUMN "symbol";
|
@ -0,0 +1,5 @@
|
|||||||
|
-- Set default value
|
||||||
|
UPDATE "SymbolProfile" SET "currency" = 'USD' WHERE "currency" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SymbolProfile" ALTER COLUMN "currency" SET NOT NULL;
|
@ -74,14 +74,11 @@ model Order {
|
|||||||
accountId String?
|
accountId String?
|
||||||
accountUserId String?
|
accountUserId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency String?
|
|
||||||
dataSource DataSource?
|
|
||||||
date DateTime
|
date DateTime
|
||||||
fee Float
|
fee Float
|
||||||
id String @default(uuid())
|
id String @default(uuid())
|
||||||
isDraft Boolean @default(false)
|
isDraft Boolean @default(false)
|
||||||
quantity Float
|
quantity Float
|
||||||
symbol String?
|
|
||||||
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
|
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
|
||||||
symbolProfileId String
|
symbolProfileId String
|
||||||
type Type
|
type Type
|
||||||
@ -119,7 +116,7 @@ model SymbolProfile {
|
|||||||
assetSubClass AssetSubClass?
|
assetSubClass AssetSubClass?
|
||||||
countries Json?
|
countries Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency String?
|
currency String
|
||||||
dataSource DataSource
|
dataSource DataSource
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String?
|
name String?
|
||||||
|
@ -192,14 +192,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
||||||
fee: 30,
|
fee: 30,
|
||||||
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
||||||
quantity: 50,
|
quantity: 50,
|
||||||
symbol: 'TSLA',
|
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', // TSLA
|
||||||
symbolProfileId: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 42.97,
|
unitPrice: 42.97,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -207,14 +204,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
||||||
fee: 29.9,
|
fee: 29.9,
|
||||||
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
||||||
quantity: 0.5614682,
|
quantity: 0.5614682,
|
||||||
symbol: 'BTCUSD',
|
symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', // BTCUSD
|
||||||
symbolProfileId: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 3562.089535970158,
|
unitPrice: 3562.089535970158,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -222,14 +216,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
||||||
fee: 80.79,
|
fee: 80.79,
|
||||||
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
symbol: 'AMZN',
|
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc', // AMZN
|
||||||
symbolProfileId: '2bd26362-136e-411c-b578-334084b4cdcc',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 2021.99,
|
unitPrice: 2021.99,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -237,14 +228,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
|
||||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 144.38,
|
unitPrice: 144.38,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -252,14 +240,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
|
||||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 147.99,
|
unitPrice: 147.99,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -267,14 +252,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
|
||||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 151.41,
|
unitPrice: 151.41,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -282,14 +264,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
|
||||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 177.69,
|
unitPrice: 177.69,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
@ -297,14 +276,11 @@ async function main() {
|
|||||||
{
|
{
|
||||||
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
accountUserId: userDemo.id,
|
accountUserId: userDemo.id,
|
||||||
currency: 'USD',
|
|
||||||
dataSource: DataSource.YAHOO,
|
|
||||||
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
||||||
fee: 19.9,
|
fee: 19.9,
|
||||||
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
symbol: 'VTI',
|
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', // VTI
|
||||||
symbolProfileId: '7d9c8540-061e-4e7e-b019-0d0f4a84e796',
|
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 203.15,
|
unitPrice: 203.15,
|
||||||
userId: userDemo.id
|
userId: userDemo.id
|
||||||
|
Reference in New Issue
Block a user