Browse Source

Optimize stock split lookup by symbol profile ID

pull/7568/head
David Requeno 3 weeks ago
parent
commit
a79a114da1
  1. 38
      apps/api/src/app/activities/activities.service.spec.ts
  2. 29
      apps/api/src/app/activities/activities.service.ts
  3. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  4. 46
      apps/api/src/services/asset-profile-split/asset-profile-split.service.spec.ts
  5. 33
      apps/api/src/services/asset-profile-split/asset-profile-split.service.ts

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

@ -14,11 +14,11 @@ import { ActivitiesService } from './activities.service';
describe('ActivitiesService', () => {
let activitiesService: ActivitiesService;
let getSplitsByAssetProfiles: jest.Mock;
let getSplitsBySymbolProfileIds: jest.Mock;
let accountService: { getCashDetails: jest.Mock };
beforeEach(() => {
getSplitsByAssetProfiles = jest.fn();
getSplitsBySymbolProfileIds = jest.fn();
accountService = { getCashDetails: jest.fn() };
activitiesService = new ActivitiesService(
@ -33,7 +33,7 @@ describe('ActivitiesService', () => {
null,
null,
null,
{ getSplitsByAssetProfiles } as unknown as AssetProfileSplitService
{ getSplitsBySymbolProfileIds } as unknown as AssetProfileSplitService
);
});
@ -133,10 +133,10 @@ describe('ActivitiesService', () => {
activities: [activity],
count: 1
});
getSplitsByAssetProfiles.mockResolvedValue(
getSplitsBySymbolProfileIds.mockResolvedValue(
new Map([
['YAHOO-MSFT', [split]],
['MANUAL-AAPL', [split]]
['YAHOO-MSFT-profile', [split]],
['MANUAL-AAPL-profile', [split]]
])
);
@ -175,8 +175,8 @@ describe('ActivitiesService', () => {
activities: [activity],
count: 1
});
getSplitsByAssetProfiles.mockResolvedValue(
new Map([['YAHOO-AAPL', [split]]])
getSplitsBySymbolProfileIds.mockResolvedValue(
new Map([['YAHOO-AAPL-profile', [split]]])
);
const result =
@ -192,8 +192,8 @@ describe('ActivitiesService', () => {
userId: 'user-id',
withExcludedAccountsAndActivities: false
});
expect(getSplitsByAssetProfiles).toHaveBeenCalledWith([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
expect(getSplitsBySymbolProfileIds).toHaveBeenCalledWith([
'YAHOO-AAPL-profile'
]);
expect(result.activities[0]).toMatchObject({
quantity: 20,
@ -223,8 +223,8 @@ describe('ActivitiesService', () => {
count: 1
});
accountService.getCashDetails.mockResolvedValue({ accounts: [] });
getSplitsByAssetProfiles.mockResolvedValue(
new Map([['YAHOO-AAPL', [split]]])
getSplitsBySymbolProfileIds.mockResolvedValue(
new Map([['YAHOO-AAPL-profile', [split]]])
);
const result =
@ -234,8 +234,8 @@ describe('ActivitiesService', () => {
withCash: true
});
expect(getSplitsByAssetProfiles).toHaveBeenCalledWith([
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }
expect(getSplitsBySymbolProfileIds).toHaveBeenCalledWith([
'YAHOO-AAPL-profile'
]);
expect(result.activities).toEqual([
expect.objectContaining({
@ -254,13 +254,8 @@ describe('ActivitiesService', () => {
activities: [activity],
count: 1
});
getSplitsByAssetProfiles.mockResolvedValue(
new Map([
[
`${activity.assetProfile.dataSource}-${activity.assetProfile.symbol}`,
splits
]
])
getSplitsBySymbolProfileIds.mockResolvedValue(
new Map([[activity.assetProfile.id, splits]])
);
const result =
@ -298,6 +293,7 @@ function createActivity({
assetSubClass,
currency,
dataSource,
id: `${dataSource}-${symbol}-profile`,
symbol
},
date: parseDate(date),

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

@ -60,7 +60,7 @@ import {
} from '@prisma/client';
import { Big } from 'big.js';
import { endOfToday } from 'date-fns';
import { groupBy, uniqBy } from 'lodash';
import { groupBy, uniq, uniqBy } from 'lodash';
import { randomUUID } from 'node:crypto';
@Injectable()
@ -932,36 +932,27 @@ export class ActivitiesService {
withExcludedAccountsAndActivities: false // TODO
});
const assetProfiles = uniqBy(
const symbolProfileIds = uniq(
activities.activities.map(({ assetProfile }) => {
return {
dataSource: assetProfile.dataSource,
symbol: assetProfile.symbol
};
}),
({ dataSource, symbol }) => {
return getAssetProfileIdentifier({ dataSource, symbol });
}
return assetProfile.id;
})
);
const splitsByAssetProfile =
await this.assetProfileSplitService.getSplitsByAssetProfiles(
assetProfiles
const splitsBySymbolProfileId =
await this.assetProfileSplitService.getSplitsBySymbolProfileIds(
symbolProfileIds
);
const hasSplits = [...splitsByAssetProfile.values()].some((splits) => {
const hasSplits = [...splitsBySymbolProfileId.values()].some((splits) => {
return splits.length > 0;
});
if (hasSplits) {
activities.activities = activities.activities.map((activity) => {
const key = getAssetProfileIdentifier({
dataSource: activity.assetProfile.dataSource,
symbol: activity.assetProfile.symbol
});
const key = activity.assetProfile.id;
return adjustActivityBySplits(
activity,
splitsByAssetProfile.get(key) ?? []
splitsBySymbolProfileId.get(key) ?? []
);
});
}

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -135,7 +135,7 @@ describe('PortfolioCalculator', () => {
null,
null,
{
getSplitsByAssetProfiles: jest.fn().mockResolvedValue(new Map())
getSplitsBySymbolProfileIds: jest.fn().mockResolvedValue(new Map())
} as unknown as AssetProfileSplitService
);

46
apps/api/src/services/asset-profile-split/asset-profile-split.service.spec.ts

@ -1,6 +1,4 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client';
@ -58,50 +56,36 @@ describe('AssetProfileSplitService', () => {
});
});
describe('getSplitsByAssetProfiles', () => {
describe('getSplitsBySymbolProfileIds', () => {
it('fetches and groups all splits with one ordered query', async () => {
const assetProfiles: AssetProfileIdentifier[] = [
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' },
{ dataSource: DataSource.YAHOO, symbol: 'MSFT' }
];
const symbolProfileIds = ['aapl-profile', 'msft-profile'];
const aaplSplit = createSplit({
date: new Date('2020-08-31'),
symbolProfile: assetProfiles[0]
symbolProfileId: symbolProfileIds[0]
});
const msftSplit = createSplit({
date: new Date('2021-09-16'),
symbolProfile: assetProfiles[1]
symbolProfileId: symbolProfileIds[1]
});
findMany.mockResolvedValue([
{ ...aaplSplit, symbolProfile: assetProfiles[0] },
{ ...msftSplit, symbolProfile: assetProfiles[1] }
]);
findMany.mockResolvedValue([aaplSplit, msftSplit]);
const splits =
await assetProfileSplitService.getSplitsByAssetProfiles(assetProfiles);
await assetProfileSplitService.getSplitsBySymbolProfileIds(
symbolProfileIds
);
expect(findMany).toHaveBeenCalledTimes(1);
expect(findMany).toHaveBeenCalledWith({
include: {
symbolProfile: {
select: {
dataSource: true,
symbol: true
}
}
},
orderBy: [{ date: 'asc' }],
where: {
symbolProfile: {
OR: assetProfiles
}
symbolProfileId: { in: symbolProfileIds }
}
});
expect(splits).toEqual(
new Map([
[getAssetProfileIdentifier(assetProfiles[0]), [aaplSplit]],
[getAssetProfileIdentifier(assetProfiles[1]), [msftSplit]]
[symbolProfileIds[0], [aaplSplit]],
[symbolProfileIds[1], [msftSplit]]
])
);
});
@ -169,18 +153,18 @@ describe('AssetProfileSplitService', () => {
function createSplit({
date,
symbolProfile
symbolProfileId
}: {
date: Date;
symbolProfile: AssetProfileIdentifier;
symbolProfileId: string;
}): AssetProfileSplit {
return {
createdAt: date,
date,
denominator: 1,
id: `${symbolProfile.symbol}-split`,
id: `${symbolProfileId}-split`,
numerator: 2,
symbolProfileId: `${symbolProfile.symbol}-profile`,
symbolProfileId,
updatedAt: date
};
}

33
apps/api/src/services/asset-profile-split/asset-profile-split.service.ts

@ -1,8 +1,5 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import {
getAssetProfileIdentifier,
resetHours
} from '@ghostfolio/common/helper';
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
@ -59,40 +56,28 @@ export class AssetProfileSplitService {
* Returns the splits of all given asset profiles, grouped by their
* data-source/symbol identifier.
*/
public async getSplitsByAssetProfiles(
assetProfiles: AssetProfileIdentifier[]
public async getSplitsBySymbolProfileIds(
symbolProfileIds: string[]
): Promise<Map<string, AssetProfileSplit[]>> {
const splitsByAssetProfile = new Map<string, AssetProfileSplit[]>();
for (const assetProfile of assetProfiles) {
splitsByAssetProfile.set(getAssetProfileIdentifier(assetProfile), []);
for (const symbolProfileId of symbolProfileIds) {
splitsByAssetProfile.set(symbolProfileId, []);
}
if (assetProfiles.length === 0) {
if (symbolProfileIds.length === 0) {
return splitsByAssetProfile;
}
const splits = await this.prismaService.assetProfileSplit.findMany({
include: {
symbolProfile: {
select: {
dataSource: true,
symbol: true
}
}
},
orderBy: [{ date: 'asc' }],
where: {
symbolProfile: {
OR: assetProfiles
}
symbolProfileId: { in: symbolProfileIds }
}
});
for (const { symbolProfile, ...split } of splits) {
splitsByAssetProfile
.get(getAssetProfileIdentifier(symbolProfile))
?.push(split);
for (const split of splits) {
splitsByAssetProfile.get(split.symbolProfileId)?.push(split);
}
return splitsByAssetProfile;

Loading…
Cancel
Save