diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 6e667efea..59313514f 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -926,22 +926,26 @@ export abstract class PortfolioCalculator { .mul(factor) .plus(oldAccumulatedSymbol.quantity); - if (type === 'BUY' && oldAccumulatedSymbol.investment.gte(0)) { - investment = oldAccumulatedSymbol.investment.plus( - quantity.mul(unitPrice) - ); - } else if (type === 'BUY' && oldAccumulatedSymbol.investment.lt(0)) { - investment = oldAccumulatedSymbol.investment.plus( - quantity.mul(oldAccumulatedSymbol.averagePrice) - ); - } else if (type === 'SELL' && oldAccumulatedSymbol.investment.gt(0)) { - investment = oldAccumulatedSymbol.investment.minus( - quantity.mul(oldAccumulatedSymbol.averagePrice) - ); - } else if (type === 'SELL' && oldAccumulatedSymbol.investment.lte(0)) { - investment = oldAccumulatedSymbol.investment.minus( - quantity.mul(unitPrice) - ); + if (type === 'BUY') { + if (oldAccumulatedSymbol.investment.gte(0)) { + investment = oldAccumulatedSymbol.investment.plus( + quantity.mul(unitPrice) + ); + } else { + investment = oldAccumulatedSymbol.investment.plus( + quantity.mul(oldAccumulatedSymbol.averagePrice) + ); + } + } else if (type === 'SELL') { + if (oldAccumulatedSymbol.investment.gt(0)) { + investment = oldAccumulatedSymbol.investment.minus( + quantity.mul(oldAccumulatedSymbol.averagePrice) + ); + } else { + investment = oldAccumulatedSymbol.investment.minus( + quantity.mul(unitPrice) + ); + } } currentTransactionPointItem = { diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts new file mode 100644 index 000000000..49b0f1a28 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -0,0 +1,130 @@ +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + loadActivityExportFile, + symbolProfileDummyData, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; +import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; +import { parseDate } from '@ghostfolio/common/helper'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Tag } from '@prisma/client'; +import { Big } from 'big.js'; +import { join } from 'path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let activityDtos: CreateOrderDto[]; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + activityDtos = loadActivityExportFile( + join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json') + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with BTCUSD short sell (in USD)', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); + + const activities: Activity[] = activityDtos.map((activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: activity.dataSource, + name: 'Bitcoin', + symbol: activity.symbol + }, + tags: activity.tags?.map((id) => { + return { id } as Tag; + }), + unitPriceInAssetProfileCurrency: activity.unitPrice + })); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot.positions[0].averagePrice).toEqual(Big(45000)); + }); + }); +}); diff --git a/test/import/ok/btcusd-short.json b/test/import/ok/btcusd-short.json new file mode 100644 index 000000000..a89ac070c --- /dev/null +++ b/test/import/ok/btcusd-short.json @@ -0,0 +1,42 @@ +{ + "meta": { + "date": "2021-12-12T00:00:00.000Z", + "version": "dev" + }, + "accounts": [], + "platforms": [], + "tags": [], + "activities": [ + { + "accountId": null, + "comment": null, + "fee": 4.46, + "quantity": 1, + "type": "SELL", + "unitPrice": 40000.00, + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-12-12T00:00:00.000Z", + "symbol": "BTCUSD", + "tags": [] + }, + { + "accountId": null, + "comment": null, + "fee": 4.46, + "quantity": 1, + "type": "SELL", + "unitPrice": 50000.00, + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-12-13T00:00:00.000Z", + "symbol": "BTCUSD", + "tags": [] + } + ], + "user": { + "settings": { + "currency": "USD" + } + } +}