Browse Source

Refactoring

pull/7251/head
Thomas Kaul 4 weeks ago
parent
commit
24c9ef39a6
  1. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  2. 8
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  3. 19
      apps/api/src/services/asset-profile-split/asset-profile-split.service.ts
  4. 5
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  5. 18
      libs/common/src/lib/dtos/create-asset-profile-split.dto.ts
  6. 34
      libs/common/src/lib/helper.spec.ts
  7. 9
      libs/common/src/lib/helper.ts
  8. 4
      libs/common/src/lib/validator-constraints/is-split-factor.ts
  9. 3
      prisma/migrations/20260802000000_added_asset_profile_split/migration.sql
  10. 8
      prisma/schema.prisma

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -127,6 +127,7 @@ export class AssetProfilesController {
@Post(':dataSource/:symbol/splits')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async createSplit(
@Body() data: CreateAssetProfileSplitDto,
@Param('dataSource') dataSource: DataSource,
@ -149,6 +150,7 @@ export class AssetProfilesController {
@Delete(':dataSource/:symbol/splits/:id')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async deleteSplit(
@Param('dataSource') dataSource: DataSource,
@Param('id') id: string,

8
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -25,7 +25,7 @@ import { MarketDataPreset } from '@ghostfolio/common/types';
import { Injectable, NotFoundException } from '@nestjs/common';
import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { groupBy } from 'lodash';
import { groupBy, omit } from 'lodash';
@Injectable()
export class AssetProfilesService {
@ -118,7 +118,6 @@ export class AssetProfilesService {
return {
marketData,
splits,
assetProfile: assetProfile ?? {
activitiesCount,
currency,
@ -128,7 +127,10 @@ export class AssetProfilesService {
assetClass: isCurrencyAssetProfile ? AssetClass.LIQUIDITY : undefined,
assetSubClass: isCurrencyAssetProfile ? AssetSubClass.CASH : undefined,
isActive: true
}
},
splits: splits.map((split) => {
return omit(split, 'symbolProfile');
})
};
}
public async getAssetProfiles({

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

@ -5,6 +5,10 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { AssetProfileSplit } from '@prisma/client';
export type AssetProfileSplitWithAssetProfileIdentifier = AssetProfileSplit & {
symbolProfile: AssetProfileIdentifier;
};
@Injectable()
export class AssetProfileSplitService {
public constructor(private readonly prismaService: PrismaService) {}
@ -30,16 +34,29 @@ export class AssetProfileSplitService {
return count > 0;
}
/**
* Returns the splits of the given asset profiles in ascending order by date.
* Each split carries the identifier of its asset profile so that the result
* can be grouped when querying multiple asset profiles at once.
*/
public async getSplits({
assetProfileIdentifiers
}: {
assetProfileIdentifiers: AssetProfileIdentifier[];
}): Promise<AssetProfileSplit[]> {
}): Promise<AssetProfileSplitWithAssetProfileIdentifier[]> {
if (assetProfileIdentifiers.length === 0) {
return [];
}
return this.prismaService.assetProfileSplit.findMany({
include: {
symbolProfile: {
select: {
dataSource: true,
symbol: true
}
}
},
orderBy: [
{
date: 'asc'

5
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -14,7 +14,8 @@ import {
getDateFormatString,
getStringOrNull,
getStringOrUndefined,
isCurrency
isCurrency,
isSplitFactor
} from '@ghostfolio/common/helper';
import {
AdminMarketDataDetails,
@ -208,7 +209,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
factor: new FormControl<number | null>(null, [
Validators.required,
(control: AbstractControl): ValidationErrors | null => {
return control.value > 0 && control.value !== 1
return isSplitFactor(control.value)
? null
: { invalidSplitFactor: true };
}

18
libs/common/src/lib/dtos/create-asset-profile-split.dto.ts

@ -1,11 +1,25 @@
import { IsISO8601, IsNumber, Validate } from 'class-validator';
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
import { IsSplitFactorConstraint } from '@ghostfolio/common/validator-constraints/is-split-factor';
import { IsSplitFactorConstraint } from '../validator-constraints/is-split-factor';
import { IsISO8601, IsNumber, Validate } from 'class-validator';
export class CreateAssetProfileSplitDto {
/**
* The date the split becomes effective. Activities before this date are
* adjusted by the factor.
*/
@IsISO8601()
@Validate(IsAfter1970Constraint)
date: string;
/**
* The number of shares held after the split per 1 share held before, for
* example 4 for a 4:1 split or 0.1 for a 1:10 reverse split.
*
* Only the quantity of activities is adjusted by this factor. Market data is
* already split-adjusted by the data providers and must not be adjusted
* again.
*/
@IsNumber()
@Validate(IsSplitFactorConstraint)
factor: number;

34
libs/common/src/lib/helper.spec.ts

@ -9,7 +9,8 @@ import {
getStringOrUndefined,
isAccountExcluded,
isCurrency,
isCurrencySymbol
isCurrencySymbol,
isSplitFactor
} from '@ghostfolio/common/helper';
describe('Helper', () => {
@ -280,4 +281,35 @@ describe('Helper', () => {
expect(isCurrencySymbol('')).toEqual(false);
});
});
describe('Is split factor', () => {
it('Forward split', () => {
expect(isSplitFactor(2)).toEqual(true);
expect(isSplitFactor(4)).toEqual(true);
});
it('Reverse split', () => {
expect(isSplitFactor(0.1)).toEqual(true);
expect(isSplitFactor(0.5)).toEqual(true);
});
it('Factor without effect', () => {
expect(isSplitFactor(1)).toEqual(false);
});
it('Zero or negative factor', () => {
expect(isSplitFactor(0)).toEqual(false);
expect(isSplitFactor(-2)).toEqual(false);
});
it('Non-finite factor', () => {
expect(isSplitFactor(Number.NaN)).toEqual(false);
expect(isSplitFactor(Number.POSITIVE_INFINITY)).toEqual(false);
});
it('Missing factor', () => {
expect(isSplitFactor(undefined)).toEqual(false);
expect(isSplitFactor(null)).toEqual(false);
});
});
});

9
libs/common/src/lib/helper.ts

@ -526,6 +526,15 @@ export function isRootCurrency(aCurrency: string) {
});
}
/**
* Validates the factor of a stock split, expressed as the number of shares
* held after the split per 1 share held before, for example 4 for a 4:1 split
* or 0.1 for a 1:10 reverse split. A factor of 1 would be a no-op.
*/
export function isSplitFactor(aFactor: number) {
return Number.isFinite(aFactor) && aFactor > 0 && aFactor !== 1;
}
export function isValidSearchQuery(aQuery: string) {
return aQuery?.trim().length >= SEARCH_QUERY_MINIMUM_LENGTH;
}

4
libs/common/src/lib/validator-constraints/is-split-factor.ts

@ -1,3 +1,5 @@
import { isSplitFactor } from '@ghostfolio/common/helper';
import {
ValidatorConstraint,
ValidatorConstraintInterface
@ -10,6 +12,6 @@ export class IsSplitFactorConstraint implements ValidatorConstraintInterface {
}
public validate(aFactor: number) {
return typeof aFactor === 'number' && aFactor > 0 && aFactor !== 1;
return isSplitFactor(aFactor);
}
}

3
prisma/migrations/20260802000000_added_asset_profile_split/migration.sql

@ -13,9 +13,6 @@ CREATE TABLE "AssetProfileSplit" (
-- CreateIndex
CREATE INDEX "AssetProfileSplit_date_idx" ON "AssetProfileSplit"("date");
-- CreateIndex
CREATE INDEX "AssetProfileSplit_symbolProfileId_idx" ON "AssetProfileSplit"("symbolProfileId");
-- CreateIndex
CREATE UNIQUE INDEX "AssetProfileSplit_symbolProfileId_date_key" ON "AssetProfileSplit"("symbolProfileId", "date");

8
prisma/schema.prisma

@ -116,6 +116,13 @@ model AssetProfileResolution {
@@unique([dataSourceOrigin, symbolOrigin])
}
/// A stock split of an asset profile. The factor is the number of shares held
/// after the split per 1 share held before, for example 4 for a 4:1 split or
/// 0.1 for a 1:10 reverse split.
///
/// Only the quantity of activities before the date is adjusted by the factor.
/// Market data is already split-adjusted by the data providers and must not be
/// adjusted again.
model AssetProfileSplit {
createdAt DateTime @default(now())
date DateTime
@ -127,7 +134,6 @@ model AssetProfileSplit {
@@unique([symbolProfileId, date])
@@index([date])
@@index([symbolProfileId])
}
model AuthDevice {

Loading…
Cancel
Save