mirror of https://github.com/ghostfolio/ghostfolio
65 changed files with 3850 additions and 2229 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 |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -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'; |
|||
|
|||
export interface AdminMarketDataDetails { |
|||
assetProfile: Partial<EnhancedAssetProfile>; |
|||
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'; |
|||
|
|||
export interface AssetProfileResponse { |
|||
assetProfile: Partial<EnhancedAssetProfile>; |
|||
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 }); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,21 @@ |
|||
-- Create the "EXCLUDE_FROM_ANALYSIS" tag if it does not exist yet |
|||
INSERT INTO "Tag" ("id", "name") |
|||
VALUES ('f2e868af-8333-459f-b161-cbc6544c24bd', 'EXCLUDE_FROM_ANALYSIS') |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
-- Migrate accounts with "isExcluded" to the "EXCLUDE_FROM_ANALYSIS" tag |
|||
INSERT INTO "TagsOnAccounts" ("accountId", "tagId", "updatedAt", "userId") |
|||
SELECT |
|||
"id", |
|||
'f2e868af-8333-459f-b161-cbc6544c24bd', |
|||
CURRENT_TIMESTAMP, |
|||
"userId" |
|||
FROM "Account" |
|||
WHERE "isExcluded" = true |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
-- DropIndex |
|||
DROP INDEX "Account_isExcluded_idx"; |
|||
|
|||
-- AlterTable |
|||
ALTER TABLE "Account" DROP COLUMN "isExcluded"; |
|||
@ -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