Browse Source

Merge b596abe00a into 403b97a3e3

pull/7631/merge
Aaryaman Vyas 2 days ago
committed by GitHub
parent
commit
45b5bd8570
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 281
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts
  3. 9
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

2
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 3.51.0 - 2026-08-14
### Changed ### Changed

281
apps/api/src/app/endpoints/benchmarks/benchmarks.service.spec.ts

@ -0,0 +1,281 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper';
import { BenchmarksService } from './benchmarks.service';
describe('BenchmarksService', () => {
let benchmarksService: BenchmarksService;
const mockBenchmarkService = {
calculateChangeInPercentage: jest.fn(
(startPrice: number, endPrice: number) => {
return (endPrice - startPrice) / startPrice;
}
)
};
const mockExchangeRateDataService = {
getExchangeRatesByCurrency: jest.fn().mockResolvedValue({})
};
const mockMarketDataService = {
marketDataItems: jest.fn()
};
const mockPortfolioService = {
getPerformance: jest.fn()
};
const mockSymbolService = {
get: jest.fn()
};
beforeEach(() => {
jest.clearAllMocks();
benchmarksService = new BenchmarksService(
mockBenchmarkService as unknown as BenchmarkService,
mockExchangeRateDataService as unknown as ExchangeRateDataService,
mockMarketDataService as unknown as MarketDataService,
mockPortfolioService as unknown as PortfolioService,
mockSymbolService as unknown as SymbolService
);
});
describe('getMarketDataForUser', () => {
it('should return benchmark data for a calendar year date range', async () => {
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999); // 2024-12-31T23:59:59.999
const endDate = new Date(2025, 11, 31, 23, 59, 59, 999); // 2025-12-31T23:59:59.999
// Portfolio chart starts from 2025-01-01 (exclusive boundary excludes 2024-12-31)
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [
{ date: '2025-01-01', value: 100 },
{ date: '2025-06-30', value: 110 },
{ date: '2025-12-31', value: 120 }
]
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
// Market data includes 2024-12-31 (start date) because our fix adds it to the query
mockMarketDataService.marketDataItems.mockResolvedValue([
{
date: new Date(Date.UTC(2024, 11, 31)),
marketPrice: 100
},
{
date: new Date(Date.UTC(2025, 0, 1)),
marketPrice: 100
},
{
date: new Date(Date.UTC(2025, 5, 30)),
marketPrice: 110
},
{
date: new Date(Date.UTC(2025, 11, 31)),
marketPrice: 120
}
]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: '2025',
endDate,
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
// Verify the market data query includes resetHours(startDate)
const marketDataCall =
mockMarketDataService.marketDataItems.mock.calls[0][0];
const dateInArray = marketDataCall.where.date.in;
// The start date (2024-12-31) should be included in the query
const startDateNormalized = resetHours(startDate);
expect(dateInArray).toContainEqual(startDateNormalized);
// Chart dates should also be included
expect(dateInArray.length).toBeGreaterThanOrEqual(4); // startDate + 3 chart dates
// Should return non-empty market data
expect(result.marketData.length).toBeGreaterThan(0);
expect(result.marketData[0].date).toBe('2024-12-31');
});
it('should return benchmark data for a named date range (ytd)', async () => {
const startDate = new Date(Date.UTC(2025, 11, 31)); // 2025-12-31T00:00:00.000Z (ytd start)
const endDate = new Date(); // now
// Portfolio chart includes 2025-12-31 (midnight, inclusive boundary)
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [
{ date: '2025-12-31', value: 100 },
{ date: '2026-01-15', value: 105 },
{ date: '2026-08-15', value: 120 }
]
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
mockMarketDataService.marketDataItems.mockResolvedValue([
{
date: new Date(Date.UTC(2025, 11, 31)),
marketPrice: 100
},
{
date: new Date(Date.UTC(2026, 0, 15)),
marketPrice: 105
},
{
date: new Date(Date.UTC(2026, 7, 15)),
marketPrice: 120
}
]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: 'ytd',
endDate,
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
expect(result.marketData.length).toBeGreaterThan(0);
expect(result.marketData[0].date).toBe('2025-12-31');
});
it('should return empty market data when market price at start date is unavailable', async () => {
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999);
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [{ date: '2025-01-01', value: 100 }]
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
// No market data at all
mockMarketDataService.marketDataItems.mockResolvedValue([]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: '2025',
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
expect(result.marketData).toEqual([]);
});
it('should return empty market data when symbol profile is not found', async () => {
const startDate = new Date(2024, 11, 31, 23, 59, 59, 999);
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [{ date: '2025-01-01', value: 100 }]
});
mockSymbolService.get.mockResolvedValue(null);
mockMarketDataService.marketDataItems.mockResolvedValue([
{
date: new Date(Date.UTC(2024, 11, 31)),
marketPrice: 100
}
]);
const result = await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange: '2025',
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
expect(result.marketData).toEqual([]);
});
it('should include the start date in the market data query for all date ranges', async () => {
const testCases = [
{
dateRange: '2025' as const,
startDate: new Date(2024, 11, 31, 23, 59, 59, 999),
expectedQueryDate: new Date(Date.UTC(2024, 11, 31))
},
{
dateRange: 'ytd' as const,
startDate: new Date(Date.UTC(2025, 11, 31)),
expectedQueryDate: new Date(Date.UTC(2025, 11, 31))
},
{
dateRange: '1y' as const,
startDate: new Date(Date.UTC(2025, 7, 15)),
expectedQueryDate: new Date(Date.UTC(2025, 7, 15))
}
];
for (const { dateRange, startDate, expectedQueryDate } of testCases) {
jest.clearAllMocks();
mockPortfolioService.getPerformance.mockResolvedValue({
chart: [{ date: '2025-01-01', value: 100 }]
});
mockSymbolService.get.mockResolvedValue({
currency: 'USD',
marketPrice: 150
});
mockMarketDataService.marketDataItems.mockResolvedValue([
{
date: expectedQueryDate,
marketPrice: 100
},
{
date: new Date(Date.UTC(2025, 0, 1)),
marketPrice: 105
}
]);
await benchmarksService.getMarketDataForUser({
dataSource: 'YAHOO',
dateRange,
filters: [],
startDate,
symbol: 'VOO',
userId: 'user-1',
userSettings: { baseCurrency: 'USD' }
});
const marketDataCall =
mockMarketDataService.marketDataItems.mock.calls[0][0];
const dateInArray = marketDataCall.where.date.in;
expect(dateInArray).toContainEqual(resetHours(startDate));
}
});
});
});

9
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

@ -72,9 +72,12 @@ export class BenchmarksService {
dataSource, dataSource,
symbol, symbol,
date: { date: {
in: chart.map(({ date }) => { in: [
return resetHours(parseDate(date)); resetHours(startDate),
}) ...chart.map(({ date }) => {
return resetHours(parseDate(date));
})
]
} }
} }
}) })

Loading…
Cancel
Save