create base structure for portfolio rewrite

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
pull/239/head
Valentin Zickner 3 years ago committed by Thomas
parent 5c0f710563
commit 9de56c32ac

@ -0,0 +1,60 @@
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
jest.mock('../../services/exchange-rate-data.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
exchangeRateDataService: jest.fn().mockImplementation(() => {
return {
toCurrency: (aValue: number,
aFromCurrency: Currency,
aToCurrency: Currency) => {
return 1 * aValue;
}
}
})
};
});
// https://jestjs.io/docs/manual-mocks#mocking-node-modules
// jest.mock('?', () => {
// return {
// // eslint-disable-next-line @typescript-eslint/naming-convention
// prismaService: jest.fn().mockImplementation(() => {
// return {
// marketData: {
// findFirst: (data: any) => {
// return {
// marketPrice: 100
// };
// }
// }
// };
// })
// };
// });
xdescribe('CurrentRateService', () => {
let exchangeRateDataService: ExchangeRateDataService;
let prismaService: PrismaService;
beforeEach(() => {
exchangeRateDataService = new ExchangeRateDataService(undefined);
prismaService = new PrismaService();
});
it('getValue', () => {
const currentRateService = new CurrentRateService(exchangeRateDataService, prismaService);
expect(currentRateService.getValue({
date: new Date(),
symbol: 'AIA',
currency: Currency.USD,
userCurrency: Currency.CHF
})).toEqual(0);
});
});

@ -0,0 +1,37 @@
import { Currency } from '@prisma/client';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
export class CurrentRateService {
public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService,
private prisma: PrismaService
) {}
/**
* TODO: @dtslvr
*/
public async getValue({date, symbol, currency, userCurrency}: GetValueParams): Promise<number> {
const marketData = await this.prisma.marketData.findFirst({
select: { date: true, marketPrice: true },
where: { date: date, symbol: symbol }
});
console.log(marketData); // { date: Date, marketPrice: number }
return this.exchangeRateDataService.toCurrency(
marketData.marketPrice,
currency,
userCurrency
);
}
}
export interface GetValueParams {
date: Date;
symbol: string;
currency: Currency;
userCurrency: Currency;
}

@ -0,0 +1,459 @@
import { PortfolioCalculator, PortfolioOrder } from '@ghostfolio/api/app/core/portfolio-calculator';
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
import { Currency } from '@prisma/client';
import { OrderType } from '@ghostfolio/api/models/order-type';
import Big from 'big.js';
jest.mock('./current-rate.service.ts', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return {
getValue: (date: Date, symbol: string, currency: Currency) => {
return 4;
}
};
})
};
});
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null);
});
describe('calculate transaction points', () => {
it('with orders of only one symbol', () => {
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersVTI);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD
}]
}
]);
});
it('with two orders at the same day of the same type', () => {
const orders = [
...ordersVTI,
{
date: '2021-02-01',
quantity: new Big('20'),
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('197.15'),
currency: Currency.USD
}
];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('35'),
symbol: 'VTI',
investment: new Big('6627.05'),
currency: Currency.USD
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('45'),
symbol: 'VTI',
investment: new Big('8403.95'),
currency: Currency.USD
}]
}
]);
});
it('with additional order', () => {
const orders = [
...ordersVTI,
{
date: '2019-09-01',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
unitPrice: new Big('2021.99'),
currency: Currency.USD
}
];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2019-09-01',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD
}]
}
]);
});
it('with additional buy & sell', () => {
const orders = [
...ordersVTI,
{
date: '2019-09-01',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
unitPrice: new Big('2021.99'),
currency: Currency.USD
},
{
date: '2020-08-02',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Sell,
unitPrice: new Big('2412.23'),
currency: Currency.USD
}
];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2019-09-01',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2020-08-02',
items: [{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD
}]
}
]);
});
it('with mixed symbols', () => {
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersMixedSymbols);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([
{
date: '2017-01-03',
items: [{
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD
}]
},
{
date: '2017-07-01',
items: [{
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'),
currency: Currency.USD
}, {
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD
}]
},
{
date: '2018-09-01',
items: [{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD
}, {
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'),
currency: Currency.USD
}, {
quantity: new Big('50'),
symbol: 'TSLA',
investment: new Big('2148.5'),
currency: Currency.USD
}]
}
]);
});
});
});
const ordersMixedSymbols: PortfolioOrder[] = [
{
date: '2017-01-03',
quantity: new Big('50'),
symbol: 'TSLA',
type: OrderType.Buy,
unitPrice: new Big('42.97'),
currency: Currency.USD
},
{
date: '2017-07-01',
quantity: new Big('0.5614682'),
symbol: 'BTCUSD',
type: OrderType.Buy,
unitPrice: new Big('3562.089535970158'),
currency: Currency.USD
},
{
date: '2018-09-01',
quantity: new Big('5'),
symbol: 'AMZN',
type: OrderType.Buy,
unitPrice: new Big('2021.99'),
currency: Currency.USD
}
];
const ordersVTI: PortfolioOrder[] = [
{
date: '2019-02-01',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('144.38'),
currency: Currency.USD
},
{
date: '2019-08-03',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('147.99'),
currency: Currency.USD
},
{
date: '2020-02-02',
quantity: new Big('15'),
symbol: 'VTI',
type: OrderType.Sell,
unitPrice: new Big('151.41'),
currency: Currency.USD
},
{
date: '2021-08-01',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('177.69'),
currency: Currency.USD
},
{
date: '2021-02-01',
quantity: new Big('10'),
symbol: 'VTI',
type: OrderType.Buy,
unitPrice: new Big('203.15'),
currency: Currency.USD
}
];

@ -0,0 +1,154 @@
import { Currency } from '@prisma/client';
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service';
import { OrderType } from '@ghostfolio/api/models/order-type';
import Big from 'big.js';
export class PortfolioCalculator {
private transactionPoints: TransactionPoint[];
constructor(
private currentRateService: CurrentRateService,
private currency: Currency,
orders: PortfolioOrder[]
) {
this.computeTransactionPoints(orders);
}
addOrder(order: PortfolioOrder): void {
}
deleteOrder(order: PortfolioOrder): void {
}
getPortfolioItemsAtTransactionPoints(): TransactionPoint[] {
return this.transactionPoints;
}
getCurrentPositions(): { [symbol: string]: TimelinePosition } {
return {};
}
calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] {
return null;
}
private computeTransactionPoints(orders: PortfolioOrder[]) {
orders.sort((a, b) => a.date.localeCompare(b.date));
this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const order of orders) {
const currentDate = order.date;
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[order.symbol];
const factor = this.getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) {
currentTransactionPointItem = {
quantity: order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity),
symbol: order.symbol,
investment: unitPrice.mul(order.quantity).mul(factor).add(oldAccumulatedSymbol.investment),
currency: order.currency
};
} else {
currentTransactionPointItem = {
quantity: order.quantity.mul(factor),
symbol: order.symbol,
investment: unitPrice.mul(order.quantity).mul(factor),
currency: order.currency
};
}
symbols[order.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter(transactionPointItem => transactionPointItem.symbol !== order.symbol);
if (!currentTransactionPointItem.quantity.eq(0)) {
newItems.push(currentTransactionPointItem);
} else {
delete symbols[order.symbol];
}
newItems.sort((a, b) => a.symbol.localeCompare(b.symbol));
if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = {
date: currentDate,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.items = newItems;
}
lastDate = currentDate;
}
}
private getFactor(type: OrderType) {
let factor: number;
switch (type) {
case OrderType.Buy:
factor = 1;
break;
case OrderType.Sell:
factor = -1;
break;
default:
factor = 0;
break;
}
return factor;
}
}
interface TransactionPoint {
date: string;
items: TransactionPointSymbol[];
}
interface TransactionPointSymbol {
quantity: Big;
symbol: string;
investment: Big;
currency: Currency;
}
interface TimelinePosition {
averagePrice: Big;
firstBuyDate: string;
quantity: Big;
symbol: string;
investment: Big;
marketPrice: number;
transactionCount: number;
}
type Accuracy = 'year' | 'month' | 'day';
interface TimelineSpecification {
start: Date;
accuracy: Accuracy
}
interface TimelinePeriod {
date: Date;
grossPerformance: number;
investment: number;
value: number;
}
export interface PortfolioOrder {
date: string;
quantity: Big;
symbol: string;
type: OrderType;
unitPrice: Big;
currency: Currency;
}

@ -73,6 +73,7 @@
"alphavantage": "2.2.0",
"angular-material-css-vars": "2.1.0",
"bent": "7.3.12",
"big.js": "^6.1.1",
"bootstrap": "4.6.0",
"cache-manager": "3.4.3",
"cache-manager-redis-store": "2.0.0",
@ -123,6 +124,7 @@
"@nrwl/node": "12.5.4",
"@nrwl/tao": "12.5.4",
"@nrwl/workspace": "12.5.4",
"@types/big.js": "^6.1.1",
"@types/cache-manager": "3.4.0",
"@types/jest": "26.0.20",
"@types/lodash": "4.14.168",

@ -2549,6 +2549,10 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/big.js@^6.1.1":
version "6.1.1"
integrity sha1-wr5egeDPDBwxcE47EvdQcS9kdBQ=
"@types/body-parser@*":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
@ -3943,6 +3947,10 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
big.js@^6.1.1:
version "6.1.1"
integrity sha1-Y7NbGdyXdclJke5dt2lIgGVdVTc=
bignumber.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"

Loading…
Cancel
Save