parent
4bbd17a37a
commit
982ba7377a
@ -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…
Reference in new issue