mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
22 changed files with 674 additions and 13 deletions
@ -0,0 +1,12 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
|
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { AssetProfileSplitService } from './asset-profile-split.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
exports: [AssetProfileSplitService], |
||||
|
imports: [PrismaModule], |
||||
|
providers: [AssetProfileSplitService] |
||||
|
}) |
||||
|
export class AssetProfileSplitModule {} |
||||
@ -0,0 +1,87 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
import { resetHours } from '@ghostfolio/common/helper'; |
||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { AssetProfileSplit } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class AssetProfileSplitService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
/** |
||||
|
* Deletes the split with the given id of an asset profile and returns |
||||
|
* whether it existed |
||||
|
*/ |
||||
|
public async deleteById({ |
||||
|
id, |
||||
|
symbolProfileId |
||||
|
}: { |
||||
|
id: string; |
||||
|
symbolProfileId: string; |
||||
|
}) { |
||||
|
const { count } = await this.prismaService.assetProfileSplit.deleteMany({ |
||||
|
where: { |
||||
|
id, |
||||
|
symbolProfileId |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return count > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the splits of the given asset profile in ascending order by date |
||||
|
*/ |
||||
|
public async getSplits({ |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}: AssetProfileIdentifier): Promise<AssetProfileSplit[]> { |
||||
|
return this.prismaService.assetProfileSplit.findMany({ |
||||
|
orderBy: [ |
||||
|
{ |
||||
|
date: 'asc' |
||||
|
} |
||||
|
], |
||||
|
where: { |
||||
|
symbolProfile: { |
||||
|
dataSource, |
||||
|
symbol |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async upsert({ |
||||
|
date, |
||||
|
denominator, |
||||
|
numerator, |
||||
|
symbolProfileId |
||||
|
}: { |
||||
|
date: Date; |
||||
|
denominator: number; |
||||
|
numerator: number; |
||||
|
symbolProfileId: string; |
||||
|
}): Promise<AssetProfileSplit> { |
||||
|
const dateOfSplit = resetHours(date); |
||||
|
|
||||
|
return this.prismaService.assetProfileSplit.upsert({ |
||||
|
create: { |
||||
|
denominator, |
||||
|
numerator, |
||||
|
symbolProfileId, |
||||
|
date: dateOfSplit |
||||
|
}, |
||||
|
update: { |
||||
|
denominator, |
||||
|
numerator |
||||
|
}, |
||||
|
where: { |
||||
|
symbolProfileId_date: { |
||||
|
symbolProfileId, |
||||
|
date: dateOfSplit |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970'; |
||||
|
import { IsSplitRatioConstraint } from '@ghostfolio/common/validator-constraints/is-split-ratio'; |
||||
|
|
||||
|
import { IsInt, IsISO8601, Validate } from 'class-validator'; |
||||
|
|
||||
|
export class CreateAssetProfileSplitDto { |
||||
|
/** |
||||
|
* The date the split becomes effective. Activities before this date are |
||||
|
* adjusted by the ratio. |
||||
|
*/ |
||||
|
@IsISO8601() |
||||
|
@Validate(IsAfter1970Constraint) |
||||
|
date: string; |
||||
|
|
||||
|
/** |
||||
|
* The number of shares held before the split, for example 1 for a 4:1 split |
||||
|
* or 10 for a 1:10 reverse split. |
||||
|
*/ |
||||
|
@IsInt() |
||||
|
denominator: number; |
||||
|
|
||||
|
/** |
||||
|
* The number of shares held after the split, for example 4 for a 4:1 split |
||||
|
* or 1 for a 1:10 reverse split. |
||||
|
* |
||||
|
* The resulting split factor is numerator / denominator. Both parts are kept |
||||
|
* so that the ratio stays exact, for example 1/3 for a 1:3 reverse split. |
||||
|
* |
||||
|
* Activities are adjusted by this ratio: the quantity is multiplied by it and |
||||
|
* the unit price divided by it, so that the total value of an activity stays |
||||
|
* the same. |
||||
|
* |
||||
|
* Market data is already split-adjusted by the data providers and must not be |
||||
|
* adjusted again. |
||||
|
*/ |
||||
|
@IsInt() |
||||
|
@Validate(IsSplitRatioConstraint) |
||||
|
numerator: number; |
||||
|
} |
||||
@ -1,8 +1,9 @@ |
|||||
import { MarketData } from '@prisma/client'; |
import { AssetProfileSplit, MarketData } from '@prisma/client'; |
||||
|
|
||||
import { EnhancedAssetProfile } from './enhanced-asset-profile.interface'; |
import { EnhancedAssetProfile } from './enhanced-asset-profile.interface'; |
||||
|
|
||||
export interface AdminMarketDataDetails { |
export interface AdminMarketDataDetails { |
||||
assetProfile: Partial<EnhancedAssetProfile>; |
assetProfile: Partial<EnhancedAssetProfile>; |
||||
marketData: MarketData[]; |
marketData: MarketData[]; |
||||
|
splits: AssetProfileSplit[]; |
||||
} |
} |
||||
|
|||||
@ -1,8 +1,9 @@ |
|||||
import { MarketData } from '@prisma/client'; |
import { AssetProfileSplit, MarketData } from '@prisma/client'; |
||||
|
|
||||
import { EnhancedAssetProfile } from '../enhanced-asset-profile.interface'; |
import { EnhancedAssetProfile } from '../enhanced-asset-profile.interface'; |
||||
|
|
||||
export interface AssetProfileResponse { |
export interface AssetProfileResponse { |
||||
assetProfile: Partial<EnhancedAssetProfile>; |
assetProfile: Partial<EnhancedAssetProfile>; |
||||
marketData: MarketData[]; |
marketData: MarketData[]; |
||||
|
splits: AssetProfileSplit[]; |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,18 @@ |
|||||
|
import { isSplitRatio } from '@ghostfolio/common/helper'; |
||||
|
|
||||
|
import { |
||||
|
ValidationArguments, |
||||
|
ValidatorConstraint, |
||||
|
ValidatorConstraintInterface |
||||
|
} from 'class-validator'; |
||||
|
|
||||
|
@ValidatorConstraint({ name: 'isSplitRatio' }) |
||||
|
export class IsSplitRatioConstraint implements ValidatorConstraintInterface { |
||||
|
public defaultMessage() { |
||||
|
return 'numerator and denominator must be different positive integers'; |
||||
|
} |
||||
|
|
||||
|
public validate(_: unknown, { object }: ValidationArguments) { |
||||
|
return isSplitRatio(object as { denominator: number; numerator: number }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
-- CreateTable |
||||
|
CREATE TABLE "AssetProfileSplit" ( |
||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
"date" TIMESTAMP(3) NOT NULL, |
||||
|
"denominator" INTEGER NOT NULL, |
||||
|
"id" TEXT NOT NULL, |
||||
|
"numerator" INTEGER NOT NULL, |
||||
|
"symbolProfileId" TEXT NOT NULL, |
||||
|
"updatedAt" TIMESTAMP(3) NOT NULL, |
||||
|
|
||||
|
CONSTRAINT "AssetProfileSplit_pkey" PRIMARY KEY ("id") |
||||
|
); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE INDEX "AssetProfileSplit_date_idx" ON "AssetProfileSplit"("date"); |
||||
|
|
||||
|
-- CreateIndex |
||||
|
CREATE UNIQUE INDEX "AssetProfileSplit_symbolProfileId_date_key" ON "AssetProfileSplit"("symbolProfileId", "date"); |
||||
|
|
||||
|
-- AddForeignKey |
||||
|
ALTER TABLE "AssetProfileSplit" ADD CONSTRAINT "AssetProfileSplit_symbolProfileId_fkey" FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
||||
Loading…
Reference in new issue