remove legacy portfolio.ts

pull/239/head
Valentin Zickner 3 years ago committed by Thomas
parent 4bbd17a37a
commit 982ba7377a

@ -66,26 +66,4 @@ export class ExperimentalController {
return marketData;
}
@Post('value/:dateString?')
public async getValue(
@Body() orders: CreateOrderDto[],
@Headers('Authorization') apiToken: string,
@Param('dateString') dateString: string
): Promise<Data> {
if (!isApiTokenAuthorized(apiToken)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let date = new Date();
if (dateString) {
date = parse(dateString, DATE_FORMAT, new Date());
}
return this.experimentalService.getValue(orders, date, baseCurrency);
}
}

@ -1,16 +1,9 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { Portfolio } from '@ghostfolio/api/models/portfolio';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Currency, Type } from '@prisma/client';
import { parseISO } from 'date-fns';
import { CreateOrderDto } from './create-order.dto';
import { Data } from './interfaces/data.interface';
@Injectable()
export class ExperimentalService {
@ -29,41 +22,4 @@ export class ExperimentalService {
where: { symbol: aSymbol }
});
}
public async getValue(
aOrders: CreateOrderDto[],
aDate: Date,
aBaseCurrency: Currency
): Promise<Data> {
const ordersWithPlatform: OrderWithAccount[] = aOrders.map((order) => {
return {
...order,
accountId: undefined,
accountUserId: undefined,
createdAt: new Date(),
dataSource: undefined,
date: parseISO(order.date),
fee: 0,
id: undefined,
platformId: undefined,
symbolProfileId: undefined,
type: Type.BUY,
updatedAt: undefined,
userId: undefined
};
});
const portfolio = new Portfolio(
this.accountService,
this.dataProviderService,
this.exchangeRateDataService,
this.rulesService
);
await portfolio.setOrders(ordersWithPlatform);
return {
currency: aBaseCurrency,
value: portfolio.getValue(aDate)
};
}
}

@ -1,478 +0,0 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
import { DATE_FORMAT, getUtc, getYesterday } from '@ghostfolio/common/helper';
import {
AccountType,
Currency,
DataSource,
Role,
Type,
ViewMode
} from '@prisma/client';
import { format } from 'date-fns';
import { DataProviderService } from '../services/data-provider.service';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
import { MarketState } from '../services/interfaces/interfaces';
import { RulesService } from '../services/rules.service';
import { Portfolio } from './portfolio';
jest.mock('../app/account/account.service', () => {
return {
AccountService: jest.fn().mockImplementation(() => {
return {
getCashDetails: () => Promise.resolve({ accounts: [], balance: 0 })
};
})
};
});
jest.mock('../services/data-provider.service', () => {
return {
DataProviderService: jest.fn().mockImplementation(() => {
const today = format(new Date(), DATE_FORMAT);
const yesterday = format(getYesterday(), DATE_FORMAT);
return {
get: () => {
return Promise.resolve({
BTCUSD: {
currency: Currency.USD,
dataSource: DataSource.YAHOO,
exchange: UNKNOWN_KEY,
marketPrice: 57973.008,
marketState: MarketState.open,
name: 'Bitcoin USD',
type: 'Cryptocurrency'
},
ETHUSD: {
currency: Currency.USD,
dataSource: DataSource.YAHOO,
exchange: UNKNOWN_KEY,
marketPrice: 3915.337,
marketState: MarketState.open,
name: 'Ethereum USD',
type: 'Cryptocurrency'
}
});
},
getHistorical: () => {
return Promise.resolve({
BTCUSD: {
[yesterday]: 56710.122,
[today]: 57973.008
},
ETHUSD: {
[yesterday]: 3641.984,
[today]: 3915.337
}
});
}
};
})
};
});
jest.mock('../services/exchange-rate-data.service', () => {
return {
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return {
initialize: () => Promise.resolve(),
toCurrency: (value: number) => {
return 1 * value;
}
};
})
};
});
jest.mock('../services/rules.service');
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
describe('Portfolio', () => {
let accountService: AccountService;
let dataProviderService: DataProviderService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolio: Portfolio;
let rulesService: RulesService;
beforeAll(async () => {
accountService = new AccountService(null, null, null);
dataProviderService = new DataProviderService(
null,
null,
null,
null,
null,
null
);
exchangeRateDataService = new ExchangeRateDataService(null);
rulesService = new RulesService();
await exchangeRateDataService.initialize();
portfolio = new Portfolio(
accountService,
dataProviderService,
exchangeRateDataService,
rulesService
);
portfolio.setUser({
accessToken: null,
Account: [
{
accountType: AccountType.SECURITIES,
balance: 0,
createdAt: new Date(),
currency: Currency.USD,
id: DEFAULT_ACCOUNT_ID,
isDefault: true,
name: 'Default Account',
platformId: null,
updatedAt: new Date(),
userId: USER_ID
}
],
alias: 'Test',
authChallenge: null,
createdAt: new Date(),
id: USER_ID,
provider: null,
role: Role.USER,
Settings: {
currency: Currency.CHF,
updatedAt: new Date(),
userId: USER_ID,
viewMode: ViewMode.DEFAULT
},
thirdPartyId: null,
updatedAt: new Date()
});
});
describe('works with no orders', () => {
it('should return []', () => {
expect(portfolio.get(new Date())).toEqual([]);
expect(portfolio.getFees()).toEqual(0);
expect(portfolio.getPositions(new Date())).toEqual({});
});
it('should return empty details', async () => {
const details = await portfolio.getDetails('1d');
expect(details).toMatchObject({
_GF_CASH: {
accounts: {},
allocationCurrent: NaN, // TODO
allocationInvestment: NaN, // TODO
countries: [],
currency: 'CHF',
grossPerformance: 0,
grossPerformancePercent: 0,
investment: 0,
marketPrice: 0,
marketState: 'open',
name: 'Cash',
quantity: 0,
sectors: [],
symbol: '_GF_CASH',
transactionCount: 0,
type: 'Cash',
value: 0
}
});
});
it('should return empty details', async () => {
const details = await portfolio.getDetails('max');
expect(details).toMatchObject({
_GF_CASH: {
accounts: {},
allocationCurrent: NaN, // TODO
allocationInvestment: NaN, // TODO
countries: [],
currency: 'CHF',
grossPerformance: 0,
grossPerformancePercent: 0,
investment: 0,
marketPrice: 0,
marketState: 'open',
name: 'Cash',
quantity: 0,
sectors: [],
symbol: '_GF_CASH',
transactionCount: 0,
type: 'Cash',
value: 0
}
});
});
});
describe('works with orders', () => {
it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 0,
date: new Date(getUtc('2018-01-05')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
quantity: 0.2,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 991.49,
updatedAt: null,
userId: USER_ID
}
]);
expect(portfolio.getFees()).toEqual(0);
expect(portfolio.getPositions(getYesterday())).toMatchObject({
ETHUSD: {
averagePrice: 991.49,
currency: Currency.USD,
firstBuyDate: '2018-01-05T00:00:00.000Z',
investment: exchangeRateDataService.toCurrency(
0.2 * 991.49,
Currency.USD,
baseCurrency
),
investmentInOriginalCurrency: 0.2 * 991.49,
// marketPrice: 3915.337,
quantity: 0.2
}
});
expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']);
});
it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 0,
date: new Date(getUtc('2018-01-05')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
quantity: 0.2,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 991.49,
updatedAt: null,
userId: USER_ID
},
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 0,
date: new Date(getUtc('2018-01-28')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
quantity: 0.3,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 1050,
updatedAt: null,
userId: USER_ID
}
]);
expect(portfolio.getFees()).toEqual(0);
expect(portfolio.getPositions(getYesterday())).toMatchObject({
ETHUSD: {
averagePrice: (0.2 * 991.49 + 0.3 * 1050) / (0.2 + 0.3),
currency: Currency.USD,
firstBuyDate: '2018-01-05T00:00:00.000Z',
investment:
exchangeRateDataService.toCurrency(
0.2 * 991.49,
Currency.USD,
baseCurrency
) +
exchangeRateDataService.toCurrency(
0.3 * 1050,
Currency.USD,
baseCurrency
),
investmentInOriginalCurrency: 0.2 * 991.49 + 0.3 * 1050,
// marketPrice: 3641.984,
quantity: 0.5
}
});
expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']);
});
it('should return ["BTCUSD", "ETHUSD"]', async () => {
await portfolio.setOrders([
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.EUR,
dataSource: DataSource.YAHOO,
date: new Date(getUtc('2017-08-16')),
fee: 2.99,
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
quantity: 0.05614682,
symbol: 'BTCUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 3562.089535970158,
updatedAt: null,
userId: USER_ID
},
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 2.99,
date: new Date(getUtc('2018-01-05')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
quantity: 0.2,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 991.49,
updatedAt: null,
userId: USER_ID
}
]);
expect(portfolio.getFees()).toEqual(
exchangeRateDataService.toCurrency(2.99, Currency.EUR, baseCurrency) +
exchangeRateDataService.toCurrency(2.99, Currency.USD, baseCurrency)
);
expect(portfolio.getPositions(getYesterday())).toMatchObject({
BTCUSD: {
averagePrice: 3562.089535970158,
currency: Currency.EUR,
firstBuyDate: '2017-08-16T00:00:00.000Z',
investment: exchangeRateDataService.toCurrency(
0.05614682 * 3562.089535970158,
Currency.EUR,
baseCurrency
),
investmentInOriginalCurrency: 0.05614682 * 3562.089535970158,
// marketPrice: 0,
quantity: 0.05614682
},
ETHUSD: {
averagePrice: 991.49,
currency: Currency.USD,
firstBuyDate: '2018-01-05T00:00:00.000Z',
investment: exchangeRateDataService.toCurrency(
0.2 * 991.49,
Currency.USD,
baseCurrency
),
investmentInOriginalCurrency: 0.2 * 991.49,
// marketPrice: 0,
quantity: 0.2
}
});
expect(portfolio.getSymbols(getYesterday())).toEqual([
'BTCUSD',
'ETHUSD'
]);
});
it('should work with buy and sell', async () => {
await portfolio.setOrders([
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 1.0,
date: new Date(getUtc('2018-01-05')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
quantity: 0.2,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 991.49,
updatedAt: null,
userId: USER_ID
},
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 1.0,
date: new Date(getUtc('2018-01-28')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
quantity: 0.1,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.SELL,
unitPrice: 1050,
updatedAt: null,
userId: USER_ID
},
{
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null,
currency: Currency.USD,
dataSource: DataSource.YAHOO,
fee: 1.0,
date: new Date(getUtc('2018-01-31')),
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
quantity: 0.2,
symbol: 'ETHUSD',
symbolProfileId: null,
type: Type.BUY,
unitPrice: 1050,
updatedAt: null,
userId: USER_ID
}
]);
expect(portfolio.getFees()).toEqual(
exchangeRateDataService.toCurrency(3, Currency.USD, baseCurrency)
);
expect(portfolio.getPositions(getYesterday())).toMatchObject({
ETHUSD: {
averagePrice:
(0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050) / (0.2 - 0.1 + 0.2),
currency: Currency.USD,
firstBuyDate: '2018-01-05T00:00:00.000Z',
investment: exchangeRateDataService.toCurrency(
0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050,
Currency.USD,
baseCurrency
),
investmentInOriginalCurrency: 0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050,
// marketPrice: 0,
quantity: 0.2 - 0.1 + 0.2
}
});
expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']);
});
});
});

@ -1,872 +0,0 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getToday,
getYesterday,
resetHours
} from '@ghostfolio/common/helper';
import {
PortfolioItem,
PortfolioPerformance,
PortfolioPosition,
PortfolioReport,
Position,
UserWithSettings
} from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
import { Currency, Prisma } from '@prisma/client';
import { continents, countries } from 'countries-list';
import {
add,
format,
getDate,
getMonth,
getYear,
isAfter,
isBefore,
isSameDay,
isToday,
isYesterday,
parseISO,
setDate,
setMonth,
sub
} from 'date-fns';
import { cloneDeep, isEmpty } from 'lodash';
import * as roundTo from 'round-to';
import { DataProviderService } from '../services/data-provider.service';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
import { IOrder, MarketState, Type } from '../services/interfaces/interfaces';
import { RulesService } from '../services/rules.service';
import { PortfolioInterface } from './interfaces/portfolio.interface';
import { Order } from './order';
import { OrderType } from './order-type';
import { AccountClusterRiskCurrentInvestment } from './rules/account-cluster-risk/current-investment';
import { AccountClusterRiskInitialInvestment } from './rules/account-cluster-risk/initial-investment';
import { AccountClusterRiskSingleAccount } from './rules/account-cluster-risk/single-account';
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
export class Portfolio implements PortfolioInterface {
private orders: Order[] = [];
private portfolioItems: PortfolioItem[] = [];
private user: UserWithSettings;
public constructor(
private accountService: AccountService,
private dataProviderService: DataProviderService,
private exchangeRateDataService: ExchangeRateDataService,
private rulesService: RulesService
) {}
public async addCurrentPortfolioItems() {
const currentData = await this.dataProviderService.get(this.getSymbols());
const currentDate = new Date();
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
const today = new Date(Date.UTC(year, month, day));
const yesterday = getYesterday();
const [portfolioItemsYesterday] = this.get(yesterday);
const positions: { [symbol: string]: Position } = {};
this.getSymbols().forEach((symbol) => {
positions[symbol] = {
symbol,
averagePrice: portfolioItemsYesterday?.positions[symbol]?.averagePrice,
currency: portfolioItemsYesterday?.positions[symbol]?.currency,
firstBuyDate: portfolioItemsYesterday?.positions[symbol]?.firstBuyDate,
investment: portfolioItemsYesterday?.positions[symbol]?.investment,
investmentInOriginalCurrency:
portfolioItemsYesterday?.positions[symbol]
?.investmentInOriginalCurrency,
marketPrice:
currentData[symbol]?.marketPrice ??
portfolioItemsYesterday.positions[symbol]?.marketPrice,
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity,
transactionCount:
portfolioItemsYesterday?.positions[symbol]?.transactionCount
};
});
if (portfolioItemsYesterday?.investment) {
const portfolioItemsLength = this.portfolioItems.push(
cloneDeep({
date: today.toISOString(),
grossPerformancePercent: 0,
investment: portfolioItemsYesterday?.investment,
positions: positions,
value: 0
})
);
// Set value after pushing today's portfolio items
this.portfolioItems[portfolioItemsLength - 1].value =
this.getValue(today);
}
return this;
}
public async addFuturePortfolioItems() {
let investment = this.getInvestment(new Date());
this.getOrders()
.filter((order) => order.getIsDraft() === true)
.forEach((order) => {
investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
const portfolioItem = this.portfolioItems.find((item) => {
return item.date === order.getDate();
});
if (portfolioItem) {
portfolioItem.investment = investment;
} else {
this.portfolioItems.push({
investment,
date: order.getDate(),
grossPerformancePercent: 0,
positions: {},
value: 0
});
}
});
return this;
}
public createFromData({
orders,
portfolioItems,
user
}: {
orders: IOrder[];
portfolioItems: PortfolioItem[];
user: UserWithSettings;
}): Portfolio {
orders.forEach(
({
account,
currency,
fee,
date,
id,
quantity,
symbol,
symbolProfile,
type,
unitPrice
}) => {
this.orders.push(
new Order({
account,
currency,
fee,
date,
id,
quantity,
symbol,
symbolProfile,
type,
unitPrice
})
);
}
);
portfolioItems.forEach(
({ date, grossPerformancePercent, investment, positions, value }) => {
this.portfolioItems.push({
date,
grossPerformancePercent,
investment,
positions,
value
});
}
);
this.setUser(user);
return this;
}
public get(aDate?: Date): PortfolioItem[] {
if (aDate) {
const filteredPortfolio = this.portfolioItems.find((item) => {
return isSameDay(aDate, new Date(item.date));
});
if (filteredPortfolio) {
return [cloneDeep(filteredPortfolio)];
}
return [];
}
return cloneDeep(this.portfolioItems);
}
public async getDetails(
aDateRange: DateRange = 'max'
): Promise<{ [symbol: string]: PortfolioPosition }> {
const dateRangeDate = this.convertDateRangeToDate(
aDateRange,
this.getMinDate()
);
const [portfolioItemsBefore] = this.get(dateRangeDate);
const [portfolioItemsNow] = await this.get(new Date());
const cashDetails = await this.accountService.getCashDetails(
this.user.id,
this.user.Settings.currency
);
const investment = this.getInvestment(new Date()) + cashDetails.balance;
const portfolioItems = this.get(new Date());
const symbols = this.getSymbols(new Date());
const value = this.getValue() + cashDetails.balance;
const details: { [symbol: string]: PortfolioPosition } = {};
const data = await this.dataProviderService.get(symbols);
symbols.forEach((symbol) => {
const accounts: PortfolioPosition['accounts'] = {};
let countriesOfSymbol: Country[];
let sectorsOfSymbol: Sector[];
const [portfolioItem] = portfolioItems;
const ordersBySymbol = this.getOrders().filter((order) => {
return order.getSymbol() === symbol;
});
ordersBySymbol.forEach((orderOfSymbol) => {
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
orderOfSymbol.getQuantity() *
portfolioItemsNow.positions[symbol].marketPrice,
orderOfSymbol.getCurrency(),
this.user.Settings.currency
);
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
orderOfSymbol.getQuantity() * orderOfSymbol.getUnitPrice(),
orderOfSymbol.getCurrency(),
this.user.Settings.currency
);
if (orderOfSymbol.getType() === 'SELL') {
currentValueOfSymbol *= -1;
originalValueOfSymbol *= -1;
}
if (
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current
) {
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].current +=
currentValueOfSymbol;
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].original +=
originalValueOfSymbol;
} else {
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = {
current: currentValueOfSymbol,
original: originalValueOfSymbol
};
}
countriesOfSymbol = (
(orderOfSymbol.getSymbolProfile()?.countries as Prisma.JsonArray) ??
[]
).map((country) => {
const { code, weight } = country as Prisma.JsonObject;
return {
code: code as string,
continent:
continents[countries[code as string]?.continent] ?? UNKNOWN_KEY,
name: countries[code as string]?.name ?? UNKNOWN_KEY,
weight: weight as number
};
});
sectorsOfSymbol = (
(orderOfSymbol.getSymbolProfile()?.sectors as Prisma.JsonArray) ?? []
).map((sector) => {
const { name, weight } = sector as Prisma.JsonObject;
return {
name: (name as string) ?? UNKNOWN_KEY,
weight: weight as number
};
});
});
let now = portfolioItemsNow.positions[symbol].marketPrice;
// 1d
let before = portfolioItemsBefore?.positions[symbol].marketPrice;
if (aDateRange === 'ytd') {
before =
portfolioItemsBefore.positions[symbol].marketPrice ||
portfolioItemsNow.positions[symbol].averagePrice;
} else if (
aDateRange === '1y' ||
aDateRange === '5y' ||
aDateRange === 'max'
) {
before = portfolioItemsNow.positions[symbol].averagePrice;
}
if (
!isBefore(
parseISO(portfolioItemsNow.positions[symbol].firstBuyDate),
parseISO(portfolioItemsBefore?.date)
)
) {
// Trade was not before the date of portfolioItemsBefore, then override it with average price
// (e.g. on same day)
before = portfolioItemsNow.positions[symbol].averagePrice;
}
if (isToday(parseISO(portfolioItemsNow.positions[symbol].firstBuyDate))) {
now = portfolioItemsNow.positions[symbol].averagePrice;
}
details[symbol] = {
...data[symbol],
accounts,
symbol,
allocationCurrent:
this.exchangeRateDataService.toCurrency(
portfolioItem.positions[symbol].quantity * now,
data[symbol]?.currency,
this.user.Settings.currency
) / value,
allocationInvestment:
portfolioItem.positions[symbol].investment / investment,
countries: countriesOfSymbol,
grossPerformance: roundTo(
portfolioItemsNow.positions[symbol].quantity * (now - before),
2
),
grossPerformancePercent: roundTo((now - before) / before, 4),
investment: portfolioItem.positions[symbol].investment,
quantity: portfolioItem.positions[symbol].quantity,
sectors: sectorsOfSymbol,
transactionCount: portfolioItem.positions[symbol].transactionCount,
value: this.exchangeRateDataService.toCurrency(
portfolioItem.positions[symbol].quantity * now,
data[symbol]?.currency,
this.user.Settings.currency
)
};
});
details[ghostfolioCashSymbol] = await this.getCashPosition({
cashDetails,
investment,
value
});
return details;
}
public getFees(aDate = new Date(0)) {
return this.orders
.filter((order) => {
// Filter out all orders before given date
return isBefore(aDate, new Date(order.getDate()));
})
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.getFee(),
order.getCurrency(),
this.user.Settings.currency
);
})
.reduce((previous, current) => previous + current, 0);
}
public getInvestment(aDate: Date): number {
return this.get(aDate)[0]?.investment || 0;
}
public getMinDate() {
const orders = this.getOrders().filter(
(order) => order.getIsDraft() === false
);
if (orders.length > 0) {
return new Date(this.orders[0].getDate());
}
return null;
}
public getPositions(aDate: Date) {
const [portfolioItem] = this.get(aDate);
if (portfolioItem) {
return portfolioItem.positions;
}
return {};
}
public getPortfolioItems() {
return this.portfolioItems;
}
public getSymbols(aDate?: Date) {
let symbols: string[] = [];
if (aDate) {
const positions = this.getPositions(aDate);
for (const symbol in positions) {
if (positions[symbol].quantity > 0) {
symbols.push(symbol);
}
}
} else {
symbols = this.orders
.filter((order) => order.getIsDraft() === false)
.map((order) => {
return order.getSymbol();
});
}
// unique values
return Array.from(new Set(symbols));
}
public getTotalBuy() {
return this.orders
.filter(
(order) => order.getIsDraft() === false && order.getType() === 'BUY'
)
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
})
.reduce((previous, current) => previous + current, 0);
}
public getTotalSell() {
return this.orders
.filter(
(order) => order.getIsDraft() === false && order.getType() === 'SELL'
)
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
})
.reduce((previous, current) => previous + current, 0);
}
public getOrders(aSymbol?: string) {
if (aSymbol) {
return this.orders.filter((order) => {
return order.getSymbol() === aSymbol;
});
}
return this.orders;
}
public getValue(aDate = getToday()) {
const positions = this.getPositions(aDate);
let value = 0;
const [portfolioItem] = this.get(aDate);
for (const symbol in positions) {
if (portfolioItem.positions[symbol]?.quantity > 0) {
if (
isBefore(
aDate,
parseISO(portfolioItem.positions[symbol]?.firstBuyDate)
) ||
portfolioItem.positions[symbol]?.marketPrice === 0
) {
value += this.exchangeRateDataService.toCurrency(
portfolioItem.positions[symbol]?.quantity *
portfolioItem.positions[symbol]?.averagePrice,
portfolioItem.positions[symbol]?.currency,
this.user.Settings.currency
);
} else {
value += this.exchangeRateDataService.toCurrency(
portfolioItem.positions[symbol]?.quantity *
portfolioItem.positions[symbol]?.marketPrice,
portfolioItem.positions[symbol]?.currency,
this.user.Settings.currency
);
}
}
}
return isFinite(value) ? value : null;
}
public async setOrders(aOrders: OrderWithAccount[]) {
this.orders = [];
// Map data
aOrders.forEach((order) => {
this.orders.push(
new Order({
account: order.Account,
currency: order.currency,
date: order.date.toISOString(),
fee: order.fee,
quantity: order.quantity,
symbol: order.symbol,
symbolProfile: order.SymbolProfile,
type: <OrderType>order.type,
unitPrice: order.unitPrice
})
);
});
await this.update();
return this;
}
public setUser(aUser: UserWithSettings) {
this.user = aUser;
return this;
}
private async getCashPosition({
cashDetails,
investment,
value
}: {
cashDetails: CashDetails;
investment: number;
value: number;
}) {
const accounts = {};
const cashValue = cashDetails.balance;
cashDetails.accounts.forEach((account) => {
accounts[account.name] = {
current: account.balance,
original: account.balance
};
});
return {
accounts,
allocationCurrent: cashValue / value,
allocationInvestment: cashValue / investment,
countries: [],
currency: Currency.CHF,
grossPerformance: 0,
grossPerformancePercent: 0,
investment: cashValue,
marketPrice: 0,
marketState: MarketState.open,
name: Type.Cash,
quantity: 0,
sectors: [],
symbol: ghostfolioCashSymbol,
type: Type.Cash,
transactionCount: 0,
value: cashValue
};
}
/**
* TODO: Refactor
*/
private async update() {
this.portfolioItems = [];
let currentDate = this.getMinDate();
if (!currentDate) {
return;
}
// Set current date to first of month
currentDate = setDate(currentDate, 1);
const historicalData = await this.dataProviderService.getHistorical(
this.getSymbols(),
'month',
currentDate,
new Date()
);
while (isBefore(currentDate, Date.now())) {
const positions: { [symbol: string]: Position } = {};
this.getSymbols().forEach((symbol) => {
positions[symbol] = {
symbol,
averagePrice: 0,
currency: undefined,
firstBuyDate: null,
investment: 0,
investmentInOriginalCurrency: 0,
marketPrice:
historicalData[symbol]?.[format(currentDate, DATE_FORMAT)]
?.marketPrice || 0,
quantity: 0,
transactionCount: 0
};
});
if (!isYesterday(currentDate) && !isToday(currentDate)) {
// Add to portfolio (ignore yesterday and today because they are added later)
this.portfolioItems.push(
cloneDeep({
date: currentDate.toISOString(),
grossPerformancePercent: 0,
investment: 0,
positions: positions,
value: 0
})
);
}
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
// Count month one up for iteration
currentDate = new Date(Date.UTC(year, month + 1, day, 0));
}
const yesterday = getYesterday();
const positions: { [symbol: string]: Position } = {};
if (isAfter(yesterday, this.getMinDate())) {
// Add yesterday
this.getSymbols().forEach((symbol) => {
positions[symbol] = {
symbol,
averagePrice: 0,
currency: undefined,
firstBuyDate: null,
investment: 0,
investmentInOriginalCurrency: 0,
marketPrice:
historicalData[symbol]?.[format(yesterday, DATE_FORMAT)]
?.marketPrice || 0,
name: '',
quantity: 0,
transactionCount: 0
};
});
this.portfolioItems.push(
cloneDeep({
positions,
date: yesterday.toISOString(),
grossPerformancePercent: 0,
investment: 0,
value: 0
})
);
}
this.updatePortfolioItems();
}
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
let currentDate = new Date();
const normalizedMinDate =
getDate(aMinDate) === 1
? aMinDate
: add(setDate(aMinDate, 1), { months: 1 });
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
switch (aDateRange) {
case '1d':
return sub(currentDate, {
days: 1
});
case 'ytd':
currentDate = setDate(currentDate, 1);
currentDate = setMonth(currentDate, 0);
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '1y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 1
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
case '5y':
currentDate = setDate(currentDate, 1);
currentDate = sub(currentDate, {
years: 5
});
return isAfter(currentDate, normalizedMinDate)
? currentDate
: undefined;
default:
// Gets handled as all data
return undefined;
}
}
private updatePortfolioItems() {
let currentDate = new Date();
const year = getYear(currentDate);
const month = getMonth(currentDate);
const day = getDate(currentDate);
currentDate = new Date(Date.UTC(year, month, day, 0));
if (this.portfolioItems?.length === 1) {
// At least one portfolio items is needed, keep it but change the date to today.
// This happens if there are only orders from today
this.portfolioItems[0].date = currentDate.toISOString();
} else {
// Only keep entries which are not before first buy date
this.portfolioItems = this.portfolioItems.filter((portfolioItem) => {
return (
isSameDay(parseISO(portfolioItem.date), this.getMinDate()) ||
isAfter(parseISO(portfolioItem.date), this.getMinDate())
);
});
}
this.orders.forEach((order) => {
if (order.getIsDraft() === false) {
let index = this.portfolioItems.findIndex((item) => {
const dateOfOrder = setDate(parseISO(order.getDate()), 1);
return isSameDay(parseISO(item.date), dateOfOrder);
});
if (index === -1) {
// if not found, we only have one order, which means we do not loop below
index = 0;
}
for (let i = index; i < this.portfolioItems.length; i++) {
// Set currency
this.portfolioItems[i].positions[order.getSymbol()].currency =
order.getCurrency();
this.portfolioItems[i].positions[
order.getSymbol()
].transactionCount += 1;
if (order.getType() === 'BUY') {
if (
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
) {
this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate =
resetHours(parseISO(order.getDate())).toISOString();
}
this.portfolioItems[i].positions[order.getSymbol()].quantity +=
order.getQuantity();
this.portfolioItems[i].positions[order.getSymbol()].investment +=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency += order.getTotal();
this.portfolioItems[i].investment +=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
} else if (order.getType() === 'SELL') {
this.portfolioItems[i].positions[order.getSymbol()].quantity -=
order.getQuantity();
if (
this.portfolioItems[i].positions[order.getSymbol()].quantity === 0
) {
this.portfolioItems[i].positions[
order.getSymbol()
].investment = 0;
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency = 0;
} else {
this.portfolioItems[i].positions[order.getSymbol()].investment -=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency -= order.getTotal();
}
this.portfolioItems[i].investment -=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
}
this.portfolioItems[i].positions[order.getSymbol()].averagePrice =
this.portfolioItems[i].positions[order.getSymbol()]
.investmentInOriginalCurrency /
this.portfolioItems[i].positions[order.getSymbol()].quantity;
const currentValue = this.getValue(
parseISO(this.portfolioItems[i].date)
);
this.portfolioItems[i].grossPerformancePercent =
currentValue / this.portfolioItems[i].investment - 1 || 0;
this.portfolioItems[i].value = currentValue;
}
}
});
}
}
Loading…
Cancel
Save