Browse Source

Bugfix/market price fallback (#7813)

* Fix market price fallback

* Update changelog
pull/8053/head^2
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
5a1ab30180
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 44
      apps/api/src/app/activities/activities.service.spec.ts
  3. 14
      apps/api/src/app/activities/activities.service.ts
  4. 16
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  5. 6
      apps/api/src/app/portfolio/current-rate.service.ts

4
CHANGELOG.md

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded `Nx` from version `23.1.1` to `23.2.1`
### Fixed
- Fixed an issue where holdings without a market price have been valued at the unit price of a dividend, a fee, an interest or a liability
## 3.79.0 - 2026-10-04
### Added

44
apps/api/src/app/activities/activities.service.spec.ts

@ -3,6 +3,7 @@ import {
activityDummyData,
assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { WHERE_ACTIVITY_NOT_DRAFT } from '@ghostfolio/api/helper/activity.helper';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -14,7 +15,11 @@ import {
import { parseDate } from '@ghostfolio/common/helper';
import { Activity, Filter } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import {
AssetProfileSplit,
DataSource,
Type as ActivityType
} from '@prisma/client';
import { Big } from 'big.js';
import { ActivitiesService } from './activities.service';
@ -381,6 +386,43 @@ describe('ActivitiesService', () => {
return result.activities[0];
}
});
describe('getLatestActivity', () => {
it('filters by type and unit price and excludes draft activities', async () => {
const findFirst = jest.fn().mockResolvedValue(null);
const service = new ActivitiesService(
null,
null,
null,
null,
null,
null,
null,
null,
null,
{ order: { findFirst } } as unknown as PrismaService,
null,
null
);
await service.getLatestActivity({
dataSource: DataSource.YAHOO,
symbol: 'AAPL',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(findFirst).toHaveBeenCalledWith({
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }],
where: {
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
type: { in: [ActivityType.BUY, ActivityType.SELL] },
unitPrice: { gt: 0 }
}
});
});
});
});
function createActivity({

14
apps/api/src/app/activities/activities.service.ts

@ -581,14 +581,16 @@ export class ActivitiesService {
public async getLatestActivity({
dataSource,
symbol
}: AssetProfileIdentifier) {
symbol,
types
}: AssetProfileIdentifier & { types: ActivityType[] }) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
},
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }],
where: {
SymbolProfile: { dataSource, symbol }
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource, symbol },
type: { in: types },
unitPrice: { gt: 0 }
}
});
}

16
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -5,7 +5,12 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData, Order } from '@prisma/client';
import {
DataSource,
MarketData,
Order,
Type as ActivityType
} from '@prisma/client';
import { addDays, subDays } from 'date-fns';
import { CurrentRateService } from './current-rate.service';
@ -220,8 +225,8 @@ describe('CurrentRateService', () => {
]);
});
it('should fall back to the unit price of the latest activity without market data', async () => {
jest
it('should fall back to the unit price of the latest buy or sell activity without market data', async () => {
const getLatestActivity = jest
.spyOn(activitiesService, 'getLatestActivity')
.mockResolvedValue({ unitPrice: 1000 } as Order);
@ -232,6 +237,11 @@ describe('CurrentRateService', () => {
dateQuery
});
expect(getLatestActivity).toHaveBeenCalledWith({
dataSource: DataSource.YAHOO,
symbol: 'AMZN',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(errors).toEqual(dataGatheringItems);
expect(values).toEqual([
{

6
apps/api/src/app/portfolio/current-rate.service.ts

@ -13,6 +13,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { Type as ActivityType } from '@prisma/client';
import { isBefore, isToday } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash-es';
@ -140,11 +141,12 @@ export class CurrentRateService {
let marketPrice = latestMarketData?.marketPrice;
if (!marketPrice) {
// Fallback to unit price of latest activity
// Fallback to unit price of latest buy or sell activity
const latestActivity =
await this.activitiesService.getLatestActivity({
dataSource,
symbol
symbol,
types: [ActivityType.BUY, ActivityType.SELL]
});
marketPrice = latestActivity?.unitPrice ?? 0;

Loading…
Cancel
Save