mirror of https://github.com/ghostfolio/ghostfolio
committed by
Thomas
4 changed files with 0 additions and 1416 deletions
@ -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