From 207d2bc52db15d8f439dc0e582fecc6de8931a79 Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Fri, 29 Mar 2024 19:54:59 +0000 Subject: [PATCH 1/6] Add SPLIT order type --- ...ate-or-update-activity-dialog.component.ts | 58 ++++++++++++++++++- .../create-or-update-activity-dialog.html | 24 +++++++- libs/ui/src/lib/i18n.ts | 1 + .../migration.sql | 2 + prisma/schema.prisma | 1 + 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20240329190053_added_split_to_order_type/migration.sql diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index b628aba46..c3f527f0c 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -279,7 +279,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { if (this.activityForm.controls['searchSymbol'].invalid) { this.data.activity.SymbolProfile = null; } else if ( - ['BUY', 'DIVIDEND', 'SELL'].includes( + ['BUY', 'DIVIDEND', 'SELL', 'SPLIT'].includes( this.activityForm.controls['type'].value ) ) { @@ -394,6 +394,62 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].setValue(false); } + } else if (type === 'SPLIT') { + this.activityForm.controls['currency'].removeValidators( + Validators.required + ); + this.activityForm.controls['currency'].updateValueAndValidity(); + + this.activityForm.controls['currencyOfFee'].removeValidators( + Validators.required + ); + this.activityForm.controls['currencyOfFee'].updateValueAndValidity(); + + this.activityForm.controls['currencyOfUnitPrice'].removeValidators( + Validators.required + ); + this.activityForm.controls[ + 'currencyOfUnitPrice' + ].updateValueAndValidity(); + + this.activityForm.controls['fee'].removeValidators( + Validators.required + ); + this.activityForm.controls['fee'].updateValueAndValidity(); + + this.activityForm.controls['feeInCustomCurrency'].removeValidators( + Validators.required + ); + this.activityForm.controls[ + 'feeInCustomCurrency' + ].updateValueAndValidity(); + + // this.activityForm.controls['searchSymbol'].removeValidators( + // Validators.required + // ); + // this.activityForm.controls['searchSymbol'].updateValueAndValidity(); + + this.activityForm.controls['unitPrice'].removeValidators( + Validators.required + ); + this.activityForm.controls['unitPrice'].updateValueAndValidity(); + + this.activityForm.controls[ + 'unitPriceInCustomCurrency' + ].removeValidators(Validators.required); + this.activityForm.controls[ + 'unitPriceInCustomCurrency' + ].updateValueAndValidity(); + + // this.activityForm.controls['dataSource'].removeValidators( + // Validators.required + // ); + // this.activityForm.controls['dataSource'].updateValueAndValidity(); + + this.activityForm.controls['quantity'].setValue(1); + + this.activityForm.controls['updateAccountBalance'].disable(); + this.activityForm.controls['updateAccountBalance'].setValue(false); } else { this.activityForm.controls['accountId'].setValidators( Validators.required diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 81c41a316..87ffc2e18 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -70,6 +70,14 @@ >Luxury items, real estate, private companies + + {{ typesTranslationMap['SPLIT'] }} + Forward split, reverse split + @@ -180,13 +188,22 @@ }" > - Quantity + + Ratio + Quantity + +
@@ -279,7 +296,8 @@ 'd-none': activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || - activityForm.controls['type']?.value === 'LIABILITY' + activityForm.controls['type']?.value === 'LIABILITY' || + activityForm.controls['type']?.value === 'SPLIT' }" > diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 9687f461c..c5af1b6e0 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -34,6 +34,7 @@ const locales = { ITEM: $localize`Valuable`, LIABILITY: $localize`Liability`, SELL: $localize`Sell`, + SPLIT: $localize`Stock Split`, // AssetClass (enum) CASH: $localize`Cash`, diff --git a/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql b/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql new file mode 100644 index 000000000..618c241b8 --- /dev/null +++ b/prisma/migrations/20240329190053_added_split_to_order_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Type" ADD VALUE 'SPLIT'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0b40109e8..3c35e2110 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -301,6 +301,7 @@ enum Type { ITEM LIABILITY SELL + SPLIT } enum ViewMode { From 2a3bc55d760e424193538b69965b0be352ac4eb5 Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Fri, 29 Mar 2024 19:59:40 +0000 Subject: [PATCH 2/6] Show activity type icon --- libs/ui/src/lib/activity-type/activity-type.component.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/ui/src/lib/activity-type/activity-type.component.html b/libs/ui/src/lib/activity-type/activity-type.component.html index 78eb2f17b..5c2abad2b 100644 --- a/libs/ui/src/lib/activity-type/activity-type.component.html +++ b/libs/ui/src/lib/activity-type/activity-type.component.html @@ -7,7 +7,8 @@ interest: activityType === 'INTEREST', item: activityType === 'ITEM', liability: activityType === 'LIABILITY', - sell: activityType === 'SELL' + sell: activityType === 'SELL', + split: activityType === 'SPLIT' }" > @if (activityType === 'BUY') { @@ -22,6 +23,8 @@ } @else if (activityType === 'SELL') { + } @else if (activityType === 'SPLIT') { + } {{ activityTypeLabel }}
From 5e96a7d20876f19594b79fb7cea22878e55d06f0 Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Fri, 29 Mar 2024 21:20:47 +0000 Subject: [PATCH 3/6] Take into account SPLIT in portfolio calculator --- apps/api/src/app/portfolio/portfolio-calculator.ts | 9 ++++++--- apps/api/src/app/portfolio/portfolio.service.ts | 4 +++- apps/api/src/helper/portfolio.helper.ts | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index d37a872c5..364ac0652 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -90,9 +90,12 @@ export class PortfolioCalculator { if (oldAccumulatedSymbol) { let investment = oldAccumulatedSymbol.investment; - const newQuantity = order.quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); + const newQuantity = + order.type === 'SPLIT' + ? order.quantity.s === 1 + ? oldAccumulatedSymbol.quantity.mul(order.quantity) + : oldAccumulatedSymbol.quantity.div(order.quantity) + : order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity); if (order.type === 'BUY') { investment = oldAccumulatedSymbol.investment.plus( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 061c4b8be..71377df49 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -741,7 +741,9 @@ export class PortfolioService { .filter((order) => { tags = tags.concat(order.tags); - return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); + return ['BUY', 'DIVIDEND', 'ITEM', 'SELL', 'SPLIT'].includes( + order.type + ); }) .map((order) => ({ currency: order.SymbolProfile.currency, diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index 01b532cbf..0535bb693 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -6,6 +6,7 @@ export function getFactor(activityType: ActivityType) { switch (activityType) { case 'BUY': case 'ITEM': + case 'SPLIT': factor = 1; break; case 'LIABILITY': From c014712e303ae9c1184aee0cf8e663afe93b2cce Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Sat, 30 Mar 2024 09:23:09 +0000 Subject: [PATCH 4/6] Allow negative quantity in DTO for SPLIT type --- apps/api/src/app/order/create-order.dto.ts | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index aecec842a..0023eab41 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -15,10 +15,33 @@ import { IsNumber, IsOptional, IsString, - Min + Min, + ValidationArguments, + ValidationOptions, + registerDecorator } from 'class-validator'; import { isString } from 'lodash'; +function IsQuantityValid(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isQuantityValid', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const order = args.object as CreateOrderDto; + return order.type !== 'SPLIT' ? value >= 0 : true; + }, + defaultMessage(args: ValidationArguments) { + return `Quantity must not be less than 0 unless the type is ${Type.SPLIT}`; + } + } + }); + }; +} + export class CreateOrderDto { @IsOptional() @IsString() @@ -54,7 +77,7 @@ export class CreateOrderDto { fee: number; @IsNumber() - @Min(0) + @IsQuantityValid() quantity: number; @IsString() From 0c89b18cc701673a4762f56b6931344e62d3a6d0 Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Sat, 30 Mar 2024 09:26:20 +0000 Subject: [PATCH 5/6] Use absolute quantity value in portfolio calculator for SPLIT --- apps/api/src/app/portfolio/portfolio-calculator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 364ac0652..15d64dff6 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -94,7 +94,7 @@ export class PortfolioCalculator { order.type === 'SPLIT' ? order.quantity.s === 1 ? oldAccumulatedSymbol.quantity.mul(order.quantity) - : oldAccumulatedSymbol.quantity.div(order.quantity) + : oldAccumulatedSymbol.quantity.div(order.quantity.abs()) : order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity); if (order.type === 'BUY') { From ca3189fcd57a5bbd569b1ab25a8101d579b6e148 Mon Sep 17 00:00:00 2001 From: Nicolas Fedor Date: Sat, 30 Mar 2024 09:43:38 +0000 Subject: [PATCH 6/6] Show correct quantity when editing --- .../create-or-update-activity-dialog.component.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index c3f527f0c..4cc405505 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -424,11 +424,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { 'feeInCustomCurrency' ].updateValueAndValidity(); - // this.activityForm.controls['searchSymbol'].removeValidators( - // Validators.required - // ); - // this.activityForm.controls['searchSymbol'].updateValueAndValidity(); - this.activityForm.controls['unitPrice'].removeValidators( Validators.required ); @@ -441,13 +436,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { 'unitPriceInCustomCurrency' ].updateValueAndValidity(); - // this.activityForm.controls['dataSource'].removeValidators( - // Validators.required - // ); - // this.activityForm.controls['dataSource'].updateValueAndValidity(); - - this.activityForm.controls['quantity'].setValue(1); - this.activityForm.controls['updateAccountBalance'].disable(); this.activityForm.controls['updateAccountBalance'].setValue(false); } else {