mirror of https://github.com/ghostfolio/ghostfolio
240 changed files with 32347 additions and 19291 deletions
@ -1,151 +0,0 @@ |
|||
{ |
|||
"root": true, |
|||
"ignorePatterns": ["**/*"], |
|||
"plugins": ["@nx"], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], |
|||
"rules": { |
|||
"@nx/enforce-module-boundaries": [ |
|||
"warn", |
|||
{ |
|||
"enforceBuildableLibDependency": true, |
|||
"allow": [], |
|||
"depConstraints": [ |
|||
{ |
|||
"sourceTag": "*", |
|||
"onlyDependOnLibsWithTags": ["*"] |
|||
} |
|||
] |
|||
} |
|||
], |
|||
"@typescript-eslint/no-extra-semi": "error", |
|||
"no-extra-semi": "off" |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.ts", "*.tsx"], |
|||
"extends": ["plugin:@nx/typescript"] |
|||
}, |
|||
{ |
|||
"files": ["*.js", "*.jsx"], |
|||
"extends": ["plugin:@nx/javascript"] |
|||
}, |
|||
{ |
|||
"files": ["*.ts"], |
|||
"plugins": ["eslint-plugin-import", "@typescript-eslint"], |
|||
"extends": [ |
|||
"plugin:@typescript-eslint/recommended-type-checked", |
|||
"plugin:@typescript-eslint/stylistic-type-checked" |
|||
], |
|||
"rules": { |
|||
"@typescript-eslint/consistent-indexed-object-style": "off", |
|||
"@typescript-eslint/dot-notation": "off", |
|||
"@typescript-eslint/explicit-member-accessibility": [ |
|||
"off", |
|||
{ |
|||
"accessibility": "explicit" |
|||
} |
|||
], |
|||
"@typescript-eslint/member-ordering": "warn", |
|||
"@typescript-eslint/naming-convention": [ |
|||
"off", |
|||
{ |
|||
"selector": "default", |
|||
"format": ["camelCase"], |
|||
"leadingUnderscore": "allow", |
|||
"trailingUnderscore": "allow" |
|||
}, |
|||
{ |
|||
"selector": ["variable", "classProperty", "typeProperty"], |
|||
"format": ["camelCase", "UPPER_CASE"], |
|||
"leadingUnderscore": "allow", |
|||
"trailingUnderscore": "allow" |
|||
}, |
|||
{ |
|||
"selector": "objectLiteralProperty", |
|||
"format": null |
|||
}, |
|||
{ |
|||
"selector": "enumMember", |
|||
"format": ["camelCase", "UPPER_CASE", "PascalCase"] |
|||
}, |
|||
{ |
|||
"selector": "typeLike", |
|||
"format": ["PascalCase"] |
|||
} |
|||
], |
|||
"@typescript-eslint/no-empty-interface": "warn", |
|||
"@typescript-eslint/no-inferrable-types": [ |
|||
"warn", |
|||
{ |
|||
"ignoreParameters": true |
|||
} |
|||
], |
|||
"@typescript-eslint/no-non-null-assertion": "warn", |
|||
"@typescript-eslint/no-shadow": [ |
|||
"warn", |
|||
{ |
|||
"hoist": "all" |
|||
} |
|||
], |
|||
"@typescript-eslint/unified-signatures": "error", |
|||
"@typescript-eslint/no-loss-of-precision": "warn", |
|||
"@typescript-eslint/no-var-requires": "warn", |
|||
"@typescript-eslint/ban-types": "warn", |
|||
"arrow-body-style": "off", |
|||
"constructor-super": "error", |
|||
"eqeqeq": ["error", "smart"], |
|||
"guard-for-in": "warn", |
|||
"id-blacklist": "off", |
|||
"id-match": "off", |
|||
"import/no-deprecated": "warn", |
|||
"no-bitwise": "error", |
|||
"no-caller": "error", |
|||
"no-debugger": "error", |
|||
"no-empty": "off", |
|||
"no-eval": "error", |
|||
"no-fallthrough": "error", |
|||
"no-new-wrappers": "error", |
|||
"no-restricted-imports": ["error", "rxjs/Rx"], |
|||
"no-undef-init": "error", |
|||
"no-underscore-dangle": "off", |
|||
"no-var": "error", |
|||
"radix": "error", |
|||
"no-unsafe-optional-chaining": "warn", |
|||
"no-extra-boolean-cast": "warn", |
|||
"no-empty-pattern": "warn", |
|||
"no-useless-catch": "warn", |
|||
"no-unsafe-finally": "warn", |
|||
"no-prototype-builtins": "warn", |
|||
"no-async-promise-executor": "warn", |
|||
"no-constant-condition": "warn", |
|||
|
|||
// The following rules are part of @typescript-eslint/recommended-type-checked |
|||
// and can be remove once solved |
|||
"@typescript-eslint/await-thenable": "warn", |
|||
"@typescript-eslint/ban-ts-comment": "warn", |
|||
"@typescript-eslint/no-base-to-string": "warn", |
|||
"@typescript-eslint/no-explicit-any": "warn", |
|||
"@typescript-eslint/no-floating-promises": "warn", |
|||
"@typescript-eslint/no-misused-promises": "warn", |
|||
"@typescript-eslint/no-redundant-type-constituents": "warn", |
|||
"@typescript-eslint/no-unnecessary-type-assertion": "warn", |
|||
"@typescript-eslint/no-unsafe-argument": "warn", |
|||
"@typescript-eslint/no-unsafe-assignment": "warn", |
|||
"@typescript-eslint/no-unsafe-enum-comparison": "warn", |
|||
"@typescript-eslint/no-unsafe-member-access": "warn", |
|||
"@typescript-eslint/no-unsafe-return": "warn", |
|||
"@typescript-eslint/no-unsafe-call": "warn", |
|||
"@typescript-eslint/require-await": "warn", |
|||
"@typescript-eslint/restrict-template-expressions": "warn", |
|||
"@typescript-eslint/unbound-method": "warn", |
|||
|
|||
// The following rules are part of @typescript-eslint/stylistic-type-checked |
|||
// and can be remove once solved |
|||
"@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true |
|||
} |
|||
} |
|||
], |
|||
"extends": ["plugin:storybook/recommended"] |
|||
} |
@ -0,0 +1,40 @@ |
|||
name: Extract locales |
|||
|
|||
on: |
|||
push: |
|||
branches: |
|||
- main |
|||
|
|||
permissions: |
|||
contents: write |
|||
pull-requests: write |
|||
|
|||
jobs: |
|||
extract_locales: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout code |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
fetch-depth: 0 |
|||
|
|||
- name: Install dependencies |
|||
run: npm ci |
|||
|
|||
- name: Extract locales |
|||
run: npm run extract-locales |
|||
|
|||
- name: Check changes |
|||
id: verify-changed-files |
|||
uses: tj-actions/verify-changed-files@v20 |
|||
|
|||
- name: Create pull request |
|||
if: steps.verify-changed-files.outputs.files_changed == 'true' |
|||
uses: peter-evans/create-pull-request@v7 |
|||
with: |
|||
author: 'github-actions[bot] <github-actions[bot]@users.noreply.github.com>' |
|||
branch: 'feature/update-locales' |
|||
commit-message: 'Update locales' |
|||
delete-branch: true |
|||
title: 'Feature/update locales' |
|||
token: ${{ secrets.GITHUB_TOKEN }} |
@ -0,0 +1,13 @@ |
|||
# Security Policy |
|||
|
|||
## Reporting Security Issues |
|||
|
|||
If you discover a security vulnerability in this repository, please report it to security[at]ghostfol.io. We will acknowledge your report and provide guidance on the next steps. |
|||
|
|||
To help us resolve the issue, please include the following details: |
|||
|
|||
- A description of the vulnerability |
|||
- Steps to reproduce the vulnerability |
|||
- Affected versions of the software |
|||
|
|||
We appreciate your responsible disclosure and will work to address the issue promptly. |
@ -1,22 +0,0 @@ |
|||
{ |
|||
"extends": "../../.eslintrc.json", |
|||
"ignorePatterns": ["!**/*"], |
|||
"rules": {}, |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], |
|||
"parserOptions": { |
|||
"project": ["apps/api/tsconfig.*?.json"] |
|||
}, |
|||
"rules": {} |
|||
}, |
|||
{ |
|||
"files": ["*.ts", "*.tsx"], |
|||
"rules": {} |
|||
}, |
|||
{ |
|||
"files": ["*.js", "*.jsx"], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,31 @@ |
|||
const baseConfig = require('../../eslint.config.cjs'); |
|||
|
|||
module.exports = [ |
|||
{ |
|||
ignores: ['**/dist'] |
|||
}, |
|||
...baseConfig, |
|||
{ |
|||
rules: {} |
|||
}, |
|||
{ |
|||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], |
|||
// Override or add rules here |
|||
rules: {}, |
|||
languageOptions: { |
|||
parserOptions: { |
|||
project: ['apps/api/tsconfig.*?.json'] |
|||
} |
|||
} |
|||
}, |
|||
{ |
|||
files: ['**/*.ts', '**/*.tsx'], |
|||
// Override or add rules here |
|||
rules: {} |
|||
}, |
|||
{ |
|||
files: ['**/*.js', '**/*.jsx'], |
|||
// Override or add rules here |
|||
rules: {} |
|||
} |
|||
]; |
@ -1,36 +0,0 @@ |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; |
|||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; |
|||
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; |
|||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { BenchmarkController } from './benchmark.controller'; |
|||
import { BenchmarkService } from './benchmark.service'; |
|||
|
|||
@Module({ |
|||
controllers: [BenchmarkController], |
|||
exports: [BenchmarkService], |
|||
imports: [ |
|||
ConfigurationModule, |
|||
DataProviderModule, |
|||
ExchangeRateDataModule, |
|||
MarketDataModule, |
|||
PrismaModule, |
|||
PropertyModule, |
|||
RedisCacheModule, |
|||
SymbolModule, |
|||
SymbolProfileModule, |
|||
TransformDataSourceInRequestModule, |
|||
TransformDataSourceInResponseModule |
|||
], |
|||
providers: [BenchmarkService] |
|||
}) |
|||
export class BenchmarkModule {} |
@ -0,0 +1,63 @@ |
|||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; |
|||
import { AccountService } from '@ghostfolio/api/app/account/account.service'; |
|||
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; |
|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; |
|||
import { UserModule } from '@ghostfolio/api/app/user/user.module'; |
|||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; |
|||
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; |
|||
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; |
|||
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
|||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; |
|||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|||
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { BenchmarksController } from './benchmarks.controller'; |
|||
import { BenchmarksService } from './benchmarks.service'; |
|||
|
|||
@Module({ |
|||
controllers: [BenchmarksController], |
|||
imports: [ |
|||
ApiModule, |
|||
ConfigurationModule, |
|||
DataProviderModule, |
|||
ExchangeRateDataModule, |
|||
ImpersonationModule, |
|||
MarketDataModule, |
|||
OrderModule, |
|||
PortfolioSnapshotQueueModule, |
|||
PrismaModule, |
|||
PropertyModule, |
|||
RedisCacheModule, |
|||
SymbolModule, |
|||
SymbolProfileModule, |
|||
TransformDataSourceInRequestModule, |
|||
TransformDataSourceInResponseModule, |
|||
UserModule |
|||
], |
|||
providers: [ |
|||
AccountBalanceService, |
|||
AccountService, |
|||
BenchmarkService, |
|||
BenchmarksService, |
|||
CurrentRateService, |
|||
MarketDataService, |
|||
PortfolioCalculatorFactory, |
|||
PortfolioService, |
|||
RulesService |
|||
] |
|||
}) |
|||
export class BenchmarksModule {} |
@ -0,0 +1,163 @@ |
|||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; |
|||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; |
|||
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
BenchmarkMarketDataDetails, |
|||
Filter |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { DateRange, UserWithSettings } from '@ghostfolio/common/types'; |
|||
|
|||
import { Injectable, Logger } from '@nestjs/common'; |
|||
import { format, isSameDay } from 'date-fns'; |
|||
import { isNumber } from 'lodash'; |
|||
|
|||
@Injectable() |
|||
export class BenchmarksService { |
|||
public constructor( |
|||
private readonly benchmarkService: BenchmarkService, |
|||
private readonly exchangeRateDataService: ExchangeRateDataService, |
|||
private readonly marketDataService: MarketDataService, |
|||
private readonly portfolioService: PortfolioService, |
|||
private readonly symbolService: SymbolService |
|||
) {} |
|||
|
|||
public async getMarketDataForUser({ |
|||
dataSource, |
|||
dateRange, |
|||
endDate = new Date(), |
|||
filters, |
|||
impersonationId, |
|||
startDate, |
|||
symbol, |
|||
user, |
|||
withExcludedAccounts |
|||
}: { |
|||
dateRange: DateRange; |
|||
endDate?: Date; |
|||
filters?: Filter[]; |
|||
impersonationId: string; |
|||
startDate: Date; |
|||
user: UserWithSettings; |
|||
withExcludedAccounts?: boolean; |
|||
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> { |
|||
const marketData: { date: string; value: number }[] = []; |
|||
const userCurrency = user.Settings.settings.baseCurrency; |
|||
const userId = user.id; |
|||
|
|||
const { chart } = await this.portfolioService.getPerformance({ |
|||
dateRange, |
|||
filters, |
|||
impersonationId, |
|||
userId, |
|||
withExcludedAccounts |
|||
}); |
|||
|
|||
const [currentSymbolItem, marketDataItems] = await Promise.all([ |
|||
this.symbolService.get({ |
|||
dataGatheringItem: { |
|||
dataSource, |
|||
symbol |
|||
} |
|||
}), |
|||
this.marketDataService.marketDataItems({ |
|||
orderBy: { |
|||
date: 'asc' |
|||
}, |
|||
where: { |
|||
dataSource, |
|||
symbol, |
|||
date: { |
|||
in: chart.map(({ date }) => { |
|||
return resetHours(parseDate(date)); |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
]); |
|||
|
|||
const exchangeRates = |
|||
await this.exchangeRateDataService.getExchangeRatesByCurrency({ |
|||
startDate, |
|||
currencies: [currentSymbolItem.currency], |
|||
targetCurrency: userCurrency |
|||
}); |
|||
|
|||
const exchangeRateAtStartDate = |
|||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
|||
format(startDate, DATE_FORMAT) |
|||
]; |
|||
|
|||
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => { |
|||
return isSameDay(date, startDate); |
|||
})?.marketPrice; |
|||
|
|||
if (!marketPriceAtStartDate) { |
|||
Logger.error( |
|||
`No historical market data has been found for ${symbol} (${dataSource}) at ${format( |
|||
startDate, |
|||
DATE_FORMAT |
|||
)}`,
|
|||
'BenchmarkService' |
|||
); |
|||
|
|||
return { marketData }; |
|||
} |
|||
|
|||
for (const marketDataItem of marketDataItems) { |
|||
const exchangeRate = |
|||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
|||
format(marketDataItem.date, DATE_FORMAT) |
|||
]; |
|||
|
|||
const exchangeRateFactor = |
|||
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) |
|||
? exchangeRate / exchangeRateAtStartDate |
|||
: 1; |
|||
|
|||
marketData.push({ |
|||
date: format(marketDataItem.date, DATE_FORMAT), |
|||
value: |
|||
marketPriceAtStartDate === 0 |
|||
? 0 |
|||
: this.benchmarkService.calculateChangeInPercentage( |
|||
marketPriceAtStartDate, |
|||
marketDataItem.marketPrice * exchangeRateFactor |
|||
) * 100 |
|||
}); |
|||
} |
|||
|
|||
const includesEndDate = isSameDay( |
|||
parseDate(marketData.at(-1).date), |
|||
endDate |
|||
); |
|||
|
|||
if (currentSymbolItem?.marketPrice && !includesEndDate) { |
|||
const exchangeRate = |
|||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[ |
|||
format(endDate, DATE_FORMAT) |
|||
]; |
|||
|
|||
const exchangeRateFactor = |
|||
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate) |
|||
? exchangeRate / exchangeRateAtStartDate |
|||
: 1; |
|||
|
|||
marketData.push({ |
|||
date: format(endDate, DATE_FORMAT), |
|||
value: |
|||
this.benchmarkService.calculateChangeInPercentage( |
|||
marketPriceAtStartDate, |
|||
currentSymbolItem.marketPrice * exchangeRateFactor |
|||
) * 100 |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
marketData |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
import { IsOptional, IsString } from 'class-validator'; |
|||
|
|||
export class CreateTagDto { |
|||
@IsString() |
|||
name: string; |
|||
|
|||
@IsOptional() |
|||
@IsString() |
|||
userId?: string; |
|||
} |
@ -0,0 +1,12 @@ |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { TagsController } from './tags.controller'; |
|||
|
|||
@Module({ |
|||
controllers: [TagsController], |
|||
imports: [PrismaModule, TagModule] |
|||
}) |
|||
export class TagsModule {} |
@ -0,0 +1,13 @@ |
|||
import { IsOptional, IsString } from 'class-validator'; |
|||
|
|||
export class UpdateTagDto { |
|||
@IsString() |
|||
id: string; |
|||
|
|||
@IsString() |
|||
name: string; |
|||
|
|||
@IsOptional() |
|||
@IsString() |
|||
userId?: string; |
|||
} |
@ -0,0 +1,10 @@ |
|||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; |
|||
import { AccountBalance } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { IsArray, IsOptional } from 'class-validator'; |
|||
|
|||
export class CreateAccountWithBalancesDto extends CreateAccountDto { |
|||
@IsArray() |
|||
@IsOptional() |
|||
balances?: AccountBalance; |
|||
} |
@ -0,0 +1,969 @@ |
|||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
|||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; |
|||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
|||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; |
|||
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
SymbolMetrics |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; |
|||
import { DateRange } from '@ghostfolio/common/types'; |
|||
|
|||
import { Logger } from '@nestjs/common'; |
|||
import { Big } from 'big.js'; |
|||
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; |
|||
import { cloneDeep, sortBy } from 'lodash'; |
|||
|
|||
export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|||
private chartDates: string[]; |
|||
|
|||
protected calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): PortfolioSnapshot { |
|||
let currentValueInBaseCurrency = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let hasErrors = false; |
|||
let netPerformance = new Big(0); |
|||
let totalFeesWithCurrencyEffect = new Big(0); |
|||
const totalInterestWithCurrencyEffect = new Big(0); |
|||
let totalInvestment = new Big(0); |
|||
let totalInvestmentWithCurrencyEffect = new Big(0); |
|||
let totalTimeWeightedInvestment = new Big(0); |
|||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
for (const currentPosition of positions) { |
|||
if (currentPosition.feeInBaseCurrency) { |
|||
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
|||
currentPosition.feeInBaseCurrency |
|||
); |
|||
} |
|||
|
|||
if (currentPosition.valueInBaseCurrency) { |
|||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
|||
currentPosition.valueInBaseCurrency |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.investment) { |
|||
totalInvestment = totalInvestment.plus(currentPosition.investment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.investmentWithCurrencyEffect |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.grossPerformance) { |
|||
grossPerformance = grossPerformance.plus( |
|||
currentPosition.grossPerformance |
|||
); |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.plus( |
|||
currentPosition.grossPerformanceWithCurrencyEffect |
|||
); |
|||
|
|||
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.timeWeightedInvestment) { |
|||
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( |
|||
currentPosition.timeWeightedInvestment |
|||
); |
|||
|
|||
totalTimeWeightedInvestmentWithCurrencyEffect = |
|||
totalTimeWeightedInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.timeWeightedInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
Logger.warn( |
|||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, |
|||
'PortfolioCalculator' |
|||
); |
|||
|
|||
hasErrors = true; |
|||
} |
|||
} |
|||
|
|||
return { |
|||
currentValueInBaseCurrency, |
|||
hasErrors, |
|||
positions, |
|||
totalFeesWithCurrencyEffect, |
|||
totalInterestWithCurrencyEffect, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
activitiesCount: this.activities.filter(({ type }) => { |
|||
return ['BUY', 'SELL'].includes(type); |
|||
}).length, |
|||
createdAt: new Date(), |
|||
errors: [], |
|||
historicalData: [], |
|||
totalLiabilitiesWithCurrencyEffect: new Big(0), |
|||
totalValuablesWithCurrencyEffect: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
protected getSymbolMetrics({ |
|||
chartDateMap, |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
marketSymbolMap, |
|||
start, |
|||
symbol |
|||
}: { |
|||
chartDateMap?: { [date: string]: boolean }; |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
} & AssetProfileIdentifier): SymbolMetrics { |
|||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; |
|||
const currentValues: { [date: string]: Big } = {}; |
|||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let fees = new Big(0); |
|||
let feesAtStartDate = new Big(0); |
|||
let feesAtStartDateWithCurrencyEffect = new Big(0); |
|||
let feesWithCurrencyEffect = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceAtStartDate = new Big(0); |
|||
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceFromSells = new Big(0); |
|||
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); |
|||
let initialValue: Big; |
|||
let initialValueWithCurrencyEffect: Big; |
|||
let investmentAtStartDate: Big; |
|||
let investmentAtStartDateWithCurrencyEffect: Big; |
|||
const investmentValuesAccumulated: { [date: string]: Big } = {}; |
|||
const investmentValuesAccumulatedWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let lastAveragePrice = new Big(0); |
|||
let lastAveragePriceWithCurrencyEffect = new Big(0); |
|||
const netPerformanceValues: { [date: string]: Big } = {}; |
|||
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
const timeWeightedInvestmentValues: { [date: string]: Big } = {}; |
|||
|
|||
const timeWeightedInvestmentValuesWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
|
|||
const totalAccountBalanceInBaseCurrency = new Big(0); |
|||
let totalDividend = new Big(0); |
|||
let totalDividendInBaseCurrency = new Big(0); |
|||
let totalInterest = new Big(0); |
|||
let totalInterestInBaseCurrency = new Big(0); |
|||
let totalInvestment = new Big(0); |
|||
let totalInvestmentFromBuyTransactions = new Big(0); |
|||
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); |
|||
let totalInvestmentWithCurrencyEffect = new Big(0); |
|||
let totalLiabilities = new Big(0); |
|||
let totalLiabilitiesInBaseCurrency = new Big(0); |
|||
let totalQuantityFromBuyTransactions = new Big(0); |
|||
let totalUnits = new Big(0); |
|||
let totalValuables = new Big(0); |
|||
let totalValuablesInBaseCurrency = new Big(0); |
|||
let valueAtStartDate: Big; |
|||
let valueAtStartDateWithCurrencyEffect: Big; |
|||
|
|||
// Clone orders to keep the original values in this.orders
|
|||
let orders: PortfolioOrderItem[] = cloneDeep( |
|||
this.activities.filter(({ SymbolProfile }) => { |
|||
return SymbolProfile.symbol === symbol; |
|||
}) |
|||
); |
|||
|
|||
if (orders.length <= 0) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: false, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: new Big(0), |
|||
totalDividend: new Big(0), |
|||
totalDividendInBaseCurrency: new Big(0), |
|||
totalInterest: new Big(0), |
|||
totalInterestInBaseCurrency: new Big(0), |
|||
totalInvestment: new Big(0), |
|||
totalInvestmentWithCurrencyEffect: new Big(0), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
const dateOfFirstTransaction = new Date(orders[0].date); |
|||
|
|||
const endDateString = format(end, DATE_FORMAT); |
|||
const startDateString = format(start, DATE_FORMAT); |
|||
|
|||
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; |
|||
const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; |
|||
|
|||
if ( |
|||
!unitPriceAtEndDate || |
|||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) |
|||
) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: true, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: new Big(0), |
|||
totalDividend: new Big(0), |
|||
totalDividendInBaseCurrency: new Big(0), |
|||
totalInterest: new Big(0), |
|||
totalInterestInBaseCurrency: new Big(0), |
|||
totalInvestment: new Big(0), |
|||
totalInvestmentWithCurrencyEffect: new Big(0), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
// Add a synthetic order at the start and the end date
|
|||
orders.push({ |
|||
date: startDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'start', |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtStartDate |
|||
}); |
|||
|
|||
orders.push({ |
|||
date: endDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'end', |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
quantity: new Big(0), |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtEndDate |
|||
}); |
|||
|
|||
let lastUnitPrice: Big; |
|||
|
|||
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; |
|||
|
|||
for (const order of orders) { |
|||
ordersByDate[order.date] = ordersByDate[order.date] ?? []; |
|||
ordersByDate[order.date].push(order); |
|||
} |
|||
|
|||
if (!this.chartDates) { |
|||
this.chartDates = Object.keys(chartDateMap).sort(); |
|||
} |
|||
|
|||
for (const dateString of this.chartDates) { |
|||
if (dateString < startDateString) { |
|||
continue; |
|||
} else if (dateString > endDateString) { |
|||
break; |
|||
} |
|||
|
|||
if (ordersByDate[dateString]?.length > 0) { |
|||
for (const order of ordersByDate[dateString]) { |
|||
order.unitPriceFromMarketData = |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; |
|||
} |
|||
} else { |
|||
orders.push({ |
|||
date: dateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, |
|||
unitPriceFromMarketData: |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice |
|||
}); |
|||
} |
|||
|
|||
const lastOrder = orders.at(-1); |
|||
|
|||
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; |
|||
} |
|||
|
|||
// Sort orders so that the start and end placeholder order are at the correct
|
|||
// position
|
|||
orders = sortBy(orders, ({ date, itemType }) => { |
|||
let sortIndex = new Date(date); |
|||
|
|||
if (itemType === 'end') { |
|||
sortIndex = addMilliseconds(sortIndex, 1); |
|||
} else if (itemType === 'start') { |
|||
sortIndex = addMilliseconds(sortIndex, -1); |
|||
} |
|||
|
|||
return sortIndex.getTime(); |
|||
}); |
|||
|
|||
const indexOfStartOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'start'; |
|||
}); |
|||
|
|||
const indexOfEndOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'end'; |
|||
}); |
|||
|
|||
let totalInvestmentDays = 0; |
|||
let sumOfTimeWeightedInvestments = new Big(0); |
|||
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|||
|
|||
for (let i = 0; i < orders.length; i += 1) { |
|||
const order = orders[i]; |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log(); |
|||
console.log(); |
|||
console.log( |
|||
i + 1, |
|||
order.date, |
|||
order.type, |
|||
order.itemType ? `(${order.itemType})` : '' |
|||
); |
|||
} |
|||
|
|||
const exchangeRateAtOrderDate = exchangeRates[order.date]; |
|||
|
|||
if (order.type === 'DIVIDEND') { |
|||
const dividend = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalDividend = totalDividend.plus(dividend); |
|||
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( |
|||
dividend.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'INTEREST') { |
|||
const interest = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalInterest = totalInterest.plus(interest); |
|||
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( |
|||
interest.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'ITEM') { |
|||
const valuables = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalValuables = totalValuables.plus(valuables); |
|||
totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus( |
|||
valuables.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'LIABILITY') { |
|||
const liabilities = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalLiabilities = totalLiabilities.plus(liabilities); |
|||
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( |
|||
liabilities.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} |
|||
|
|||
if (order.itemType === 'start') { |
|||
// Take the unit price of the order as the market price if there are no
|
|||
// orders of this symbol before the start date
|
|||
order.unitPrice = |
|||
indexOfStartOrder === 0 |
|||
? orders[i + 1]?.unitPrice |
|||
: unitPriceAtStartDate; |
|||
} |
|||
|
|||
if (order.fee) { |
|||
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); |
|||
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const unitPrice = ['BUY', 'SELL'].includes(order.type) |
|||
? order.unitPrice |
|||
: order.unitPriceFromMarketData; |
|||
|
|||
if (unitPrice) { |
|||
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); |
|||
|
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const valueOfInvestmentBeforeTransaction = totalUnits.mul( |
|||
order.unitPriceInBaseCurrency |
|||
); |
|||
|
|||
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); |
|||
|
|||
if (!investmentAtStartDate && i >= indexOfStartOrder) { |
|||
investmentAtStartDate = totalInvestment ?? new Big(0); |
|||
|
|||
investmentAtStartDateWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect ?? new Big(0); |
|||
|
|||
valueAtStartDate = valueOfInvestmentBeforeTransaction; |
|||
|
|||
valueAtStartDateWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} |
|||
|
|||
let transactionInvestment = new Big(0); |
|||
let transactionInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
if (order.type === 'BUY') { |
|||
transactionInvestment = order.quantity |
|||
.mul(order.unitPriceInBaseCurrency) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
transactionInvestmentWithCurrencyEffect = order.quantity |
|||
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
totalQuantityFromBuyTransactions = |
|||
totalQuantityFromBuyTransactions.plus(order.quantity); |
|||
|
|||
totalInvestmentFromBuyTransactions = |
|||
totalInvestmentFromBuyTransactions.plus(transactionInvestment); |
|||
|
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect = |
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (order.type === 'SELL') { |
|||
if (totalUnits.gt(0)) { |
|||
transactionInvestment = totalInvestment |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
transactionInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
} |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('order.quantity', order.quantity.toNumber()); |
|||
console.log('transactionInvestment', transactionInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'transactionInvestmentWithCurrencyEffect', |
|||
transactionInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const totalInvestmentBeforeTransaction = totalInvestment; |
|||
|
|||
const totalInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
totalInvestment = totalInvestment.plus(transactionInvestment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
|
|||
if (i >= indexOfStartOrder && !initialValue) { |
|||
if ( |
|||
i === indexOfStartOrder && |
|||
!valueOfInvestmentBeforeTransaction.eq(0) |
|||
) { |
|||
initialValue = valueOfInvestmentBeforeTransaction; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} else if (transactionInvestment.gt(0)) { |
|||
initialValue = transactionInvestment; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
transactionInvestmentWithCurrencyEffect; |
|||
} |
|||
} |
|||
|
|||
fees = fees.plus(order.feeInBaseCurrency ?? 0); |
|||
|
|||
feesWithCurrencyEffect = feesWithCurrencyEffect.plus( |
|||
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 |
|||
); |
|||
|
|||
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); |
|||
|
|||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); |
|||
|
|||
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( |
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
); |
|||
|
|||
const grossPerformanceFromSell = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrency |
|||
.minus(lastAveragePrice) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
const grossPerformanceFromSellWithCurrencyEffect = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
.minus(lastAveragePriceWithCurrencyEffect) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
grossPerformanceFromSells = grossPerformanceFromSells.plus( |
|||
grossPerformanceFromSell |
|||
); |
|||
|
|||
grossPerformanceFromSellsWithCurrencyEffect = |
|||
grossPerformanceFromSellsWithCurrencyEffect.plus( |
|||
grossPerformanceFromSellWithCurrencyEffect |
|||
); |
|||
|
|||
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactions.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( |
|||
0 |
|||
) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
'grossPerformanceFromSells', |
|||
grossPerformanceFromSells.toNumber() |
|||
); |
|||
console.log( |
|||
'grossPerformanceFromSellWithCurrencyEffect', |
|||
grossPerformanceFromSellWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const newGrossPerformance = valueOfInvestment |
|||
.minus(totalInvestment) |
|||
.plus(grossPerformanceFromSells); |
|||
|
|||
const newGrossPerformanceWithCurrencyEffect = |
|||
valueOfInvestmentWithCurrencyEffect |
|||
.minus(totalInvestmentWithCurrencyEffect) |
|||
.plus(grossPerformanceFromSellsWithCurrencyEffect); |
|||
|
|||
grossPerformance = newGrossPerformance; |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
newGrossPerformanceWithCurrencyEffect; |
|||
|
|||
if (order.itemType === 'start') { |
|||
feesAtStartDate = fees; |
|||
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; |
|||
grossPerformanceAtStartDate = grossPerformance; |
|||
|
|||
grossPerformanceAtStartDateWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect; |
|||
} |
|||
|
|||
if (i > indexOfStartOrder) { |
|||
// Only consider periods with an investment for the calculation of
|
|||
// the time weighted investment
|
|||
if ( |
|||
valueOfInvestmentBeforeTransaction.gt(0) && |
|||
['BUY', 'SELL'].includes(order.type) |
|||
) { |
|||
// Calculate the number of days since the previous order
|
|||
const orderDate = new Date(order.date); |
|||
const previousOrderDate = new Date(orders[i - 1].date); |
|||
|
|||
let daysSinceLastOrder = differenceInDays( |
|||
orderDate, |
|||
previousOrderDate |
|||
); |
|||
if (daysSinceLastOrder <= 0) { |
|||
// The time between two activities on the same day is unknown
|
|||
// -> Set it to the smallest floating point number greater than 0
|
|||
daysSinceLastOrder = Number.EPSILON; |
|||
} |
|||
|
|||
// Sum up the total investment days since the start date to calculate
|
|||
// the time weighted investment
|
|||
totalInvestmentDays += daysSinceLastOrder; |
|||
|
|||
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( |
|||
valueAtStartDate |
|||
.minus(investmentAtStartDate) |
|||
.plus(totalInvestmentBeforeTransaction) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
|
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect = |
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( |
|||
valueAtStartDateWithCurrencyEffect |
|||
.minus(investmentAtStartDateWithCurrencyEffect) |
|||
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
} |
|||
|
|||
currentValues[order.date] = valueOfInvestment; |
|||
|
|||
currentValuesWithCurrencyEffect[order.date] = |
|||
valueOfInvestmentWithCurrencyEffect; |
|||
|
|||
netPerformanceValues[order.date] = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
netPerformanceValuesWithCurrencyEffect[order.date] = |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.minus( |
|||
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) |
|||
); |
|||
|
|||
investmentValuesAccumulated[order.date] = totalInvestment; |
|||
|
|||
investmentValuesAccumulatedWithCurrencyEffect[order.date] = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
investmentValuesWithCurrencyEffect[order.date] = ( |
|||
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) |
|||
).add(transactionInvestmentWithCurrencyEffect); |
|||
|
|||
timeWeightedInvestmentValues[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('totalInvestment', totalInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'totalInvestmentWithCurrencyEffect', |
|||
totalInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformance', |
|||
grossPerformance.minus(grossPerformanceAtStartDate).toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformanceWithCurrencyEffect', |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.toNumber() |
|||
); |
|||
} |
|||
|
|||
if (i === indexOfEndOrder) { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const totalGrossPerformance = grossPerformance.minus( |
|||
grossPerformanceAtStartDate |
|||
); |
|||
|
|||
const totalGrossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.minus( |
|||
grossPerformanceAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
const totalNetPerformance = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDate = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalGrossPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentageWithCurrencyEffect = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( |
|||
0 |
|||
) |
|||
? totalGrossPerformanceWithCurrencyEffect.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnit = totalUnits.gt(0) |
|||
? fees.minus(feesAtStartDate).div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnitWithCurrencyEffect = totalUnits.gt(0) |
|||
? feesWithCurrencyEffect |
|||
.minus(feesAtStartDateWithCurrencyEffect) |
|||
.div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalNetPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentageWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
const netPerformanceWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
for (const dateRange of [ |
|||
'1d', |
|||
'1y', |
|||
'5y', |
|||
'max', |
|||
'mtd', |
|||
'wtd', |
|||
'ytd' |
|||
// TODO:
|
|||
// ...eachYearOfInterval({ end, start })
|
|||
// .filter((date) => {
|
|||
// return !isThisYear(date);
|
|||
// })
|
|||
// .map((date) => {
|
|||
// return format(date, 'yyyy');
|
|||
// })
|
|||
] as DateRange[]) { |
|||
const dateInterval = getIntervalFromDateRange(dateRange); |
|||
const endDate = dateInterval.endDate; |
|||
let startDate = dateInterval.startDate; |
|||
|
|||
if (isBefore(startDate, start)) { |
|||
startDate = start; |
|||
} |
|||
|
|||
const rangeEndDateString = format(endDate, DATE_FORMAT); |
|||
const rangeStartDateString = format(startDate, DATE_FORMAT); |
|||
|
|||
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|||
|
|||
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = |
|||
investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0); |
|||
|
|||
const grossPerformanceAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesAtDateRangeStartWithCurrencyEffect.minus( |
|||
investmentValuesAccumulatedAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
let average = new Big(0); |
|||
let dayCount = 0; |
|||
|
|||
for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { |
|||
const date = this.chartDates[i]; |
|||
|
|||
if (date > rangeEndDateString) { |
|||
continue; |
|||
} else if (date < rangeStartDateString) { |
|||
break; |
|||
} |
|||
|
|||
if ( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) |
|||
) { |
|||
average = average.add( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].add( |
|||
grossPerformanceAtDateRangeStartWithCurrencyEffect |
|||
) |
|||
); |
|||
|
|||
dayCount++; |
|||
} |
|||
} |
|||
|
|||
if (dayCount > 0) { |
|||
average = average.div(dayCount); |
|||
} |
|||
|
|||
netPerformanceWithCurrencyEffectMap[dateRange] = |
|||
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( |
|||
// If the date range is 'max', take 0 as a start value. Otherwise,
|
|||
// the value of the end of the day of the start date is taken which
|
|||
// differs from the buying price.
|
|||
dateRange === 'max' |
|||
? new Big(0) |
|||
: (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0)) |
|||
) ?? new Big(0); |
|||
|
|||
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) |
|||
? netPerformanceWithCurrencyEffectMap[dateRange].div(average) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
` |
|||
${symbol} |
|||
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( |
|||
2 |
|||
)} -> ${unitPriceAtEndDate.toFixed(2)} |
|||
Total investment: ${totalInvestment.toFixed(2)} |
|||
Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Total dividend: ${totalDividend.toFixed(2)} |
|||
Gross performance: ${totalGrossPerformance.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|||
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentageWithCurrencyEffect |
|||
.mul(100) |
|||
.toFixed(2)}% |
|||
Fees per unit: ${feesPerUnit.toFixed(2)} |
|||
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Net performance: ${totalNetPerformance.toFixed( |
|||
2 |
|||
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|||
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|||
'max' |
|||
].toFixed(2)}%` |
|||
); |
|||
} |
|||
|
|||
return { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
feesWithCurrencyEffect, |
|||
grossPerformancePercentage, |
|||
grossPerformancePercentageWithCurrencyEffect, |
|||
initialValue, |
|||
initialValueWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformancePercentage, |
|||
netPerformancePercentageWithCurrencyEffectMap, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
netPerformanceWithCurrencyEffectMap, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect, |
|||
totalAccountBalanceInBaseCurrency, |
|||
totalDividend, |
|||
totalDividendInBaseCurrency, |
|||
totalInterest, |
|||
totalInterestInBaseCurrency, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
totalLiabilities, |
|||
totalLiabilitiesInBaseCurrency, |
|||
totalValuables, |
|||
totalValuablesInBaseCurrency, |
|||
grossPerformance: totalGrossPerformance, |
|||
grossPerformanceWithCurrencyEffect: |
|||
totalGrossPerformanceWithCurrencyEffect, |
|||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), |
|||
netPerformance: totalNetPerformance, |
|||
timeWeightedInvestment: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate, |
|||
timeWeightedInvestmentWithCurrencyEffect: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
}; |
|||
} |
|||
} |
@ -1,965 +1,24 @@ |
|||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
|||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; |
|||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
|||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; |
|||
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
SymbolMetrics |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; |
|||
import { DateRange } from '@ghostfolio/common/types'; |
|||
import { PortfolioSnapshot } from '@ghostfolio/common/models'; |
|||
|
|||
import { Logger } from '@nestjs/common'; |
|||
import { Big } from 'big.js'; |
|||
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; |
|||
import { cloneDeep, sortBy } from 'lodash'; |
|||
|
|||
export class TWRPortfolioCalculator extends PortfolioCalculator { |
|||
private chartDates: string[]; |
|||
|
|||
protected calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): PortfolioSnapshot { |
|||
let currentValueInBaseCurrency = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let hasErrors = false; |
|||
let netPerformance = new Big(0); |
|||
let totalFeesWithCurrencyEffect = new Big(0); |
|||
const totalInterestWithCurrencyEffect = new Big(0); |
|||
let totalInvestment = new Big(0); |
|||
let totalInvestmentWithCurrencyEffect = new Big(0); |
|||
let totalTimeWeightedInvestment = new Big(0); |
|||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
for (const currentPosition of positions) { |
|||
if (currentPosition.feeInBaseCurrency) { |
|||
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
|||
currentPosition.feeInBaseCurrency |
|||
); |
|||
} |
|||
|
|||
if (currentPosition.valueInBaseCurrency) { |
|||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
|||
currentPosition.valueInBaseCurrency |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.investment) { |
|||
totalInvestment = totalInvestment.plus(currentPosition.investment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.investmentWithCurrencyEffect |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.grossPerformance) { |
|||
grossPerformance = grossPerformance.plus( |
|||
currentPosition.grossPerformance |
|||
); |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.plus( |
|||
currentPosition.grossPerformanceWithCurrencyEffect |
|||
); |
|||
|
|||
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.timeWeightedInvestment) { |
|||
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( |
|||
currentPosition.timeWeightedInvestment |
|||
); |
|||
|
|||
totalTimeWeightedInvestmentWithCurrencyEffect = |
|||
totalTimeWeightedInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.timeWeightedInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
Logger.warn( |
|||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, |
|||
'PortfolioCalculator' |
|||
); |
|||
|
|||
hasErrors = true; |
|||
} |
|||
} |
|||
|
|||
return { |
|||
currentValueInBaseCurrency, |
|||
hasErrors, |
|||
positions, |
|||
totalFeesWithCurrencyEffect, |
|||
totalInterestWithCurrencyEffect, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
errors: [], |
|||
historicalData: [], |
|||
totalLiabilitiesWithCurrencyEffect: new Big(0), |
|||
totalValuablesWithCurrencyEffect: new Big(0) |
|||
}; |
|||
export class TwrPortfolioCalculator extends PortfolioCalculator { |
|||
protected calculateOverallPerformance(): PortfolioSnapshot { |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
|
|||
protected getSymbolMetrics({ |
|||
chartDateMap, |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
marketSymbolMap, |
|||
start, |
|||
symbol |
|||
}: { |
|||
chartDateMap?: { [date: string]: boolean }; |
|||
protected getSymbolMetrics({}: { |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
step?: number; |
|||
} & AssetProfileIdentifier): SymbolMetrics { |
|||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; |
|||
const currentValues: { [date: string]: Big } = {}; |
|||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let fees = new Big(0); |
|||
let feesAtStartDate = new Big(0); |
|||
let feesAtStartDateWithCurrencyEffect = new Big(0); |
|||
let feesWithCurrencyEffect = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceAtStartDate = new Big(0); |
|||
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceFromSells = new Big(0); |
|||
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); |
|||
let initialValue: Big; |
|||
let initialValueWithCurrencyEffect: Big; |
|||
let investmentAtStartDate: Big; |
|||
let investmentAtStartDateWithCurrencyEffect: Big; |
|||
const investmentValuesAccumulated: { [date: string]: Big } = {}; |
|||
const investmentValuesAccumulatedWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let lastAveragePrice = new Big(0); |
|||
let lastAveragePriceWithCurrencyEffect = new Big(0); |
|||
const netPerformanceValues: { [date: string]: Big } = {}; |
|||
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
const timeWeightedInvestmentValues: { [date: string]: Big } = {}; |
|||
|
|||
const timeWeightedInvestmentValuesWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
|
|||
const totalAccountBalanceInBaseCurrency = new Big(0); |
|||
let totalDividend = new Big(0); |
|||
let totalDividendInBaseCurrency = new Big(0); |
|||
let totalInterest = new Big(0); |
|||
let totalInterestInBaseCurrency = new Big(0); |
|||
let totalInvestment = new Big(0); |
|||
let totalInvestmentFromBuyTransactions = new Big(0); |
|||
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); |
|||
let totalInvestmentWithCurrencyEffect = new Big(0); |
|||
let totalLiabilities = new Big(0); |
|||
let totalLiabilitiesInBaseCurrency = new Big(0); |
|||
let totalQuantityFromBuyTransactions = new Big(0); |
|||
let totalUnits = new Big(0); |
|||
let totalValuables = new Big(0); |
|||
let totalValuablesInBaseCurrency = new Big(0); |
|||
let valueAtStartDate: Big; |
|||
let valueAtStartDateWithCurrencyEffect: Big; |
|||
|
|||
// Clone orders to keep the original values in this.orders
|
|||
let orders: PortfolioOrderItem[] = cloneDeep( |
|||
this.activities.filter(({ SymbolProfile }) => { |
|||
return SymbolProfile.symbol === symbol; |
|||
}) |
|||
); |
|||
|
|||
if (orders.length <= 0) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: false, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: new Big(0), |
|||
totalDividend: new Big(0), |
|||
totalDividendInBaseCurrency: new Big(0), |
|||
totalInterest: new Big(0), |
|||
totalInterestInBaseCurrency: new Big(0), |
|||
totalInvestment: new Big(0), |
|||
totalInvestmentWithCurrencyEffect: new Big(0), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
const dateOfFirstTransaction = new Date(orders[0].date); |
|||
|
|||
const endDateString = format(end, DATE_FORMAT); |
|||
const startDateString = format(start, DATE_FORMAT); |
|||
|
|||
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; |
|||
const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; |
|||
|
|||
if ( |
|||
!unitPriceAtEndDate || |
|||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) |
|||
) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: true, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: new Big(0), |
|||
totalDividend: new Big(0), |
|||
totalDividendInBaseCurrency: new Big(0), |
|||
totalInterest: new Big(0), |
|||
totalInterestInBaseCurrency: new Big(0), |
|||
totalInvestment: new Big(0), |
|||
totalInvestmentWithCurrencyEffect: new Big(0), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
// Add a synthetic order at the start and the end date
|
|||
orders.push({ |
|||
date: startDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'start', |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtStartDate |
|||
}); |
|||
|
|||
orders.push({ |
|||
date: endDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'end', |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
quantity: new Big(0), |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtEndDate |
|||
}); |
|||
|
|||
let lastUnitPrice: Big; |
|||
|
|||
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; |
|||
|
|||
for (const order of orders) { |
|||
ordersByDate[order.date] = ordersByDate[order.date] ?? []; |
|||
ordersByDate[order.date].push(order); |
|||
} |
|||
|
|||
if (!this.chartDates) { |
|||
this.chartDates = Object.keys(chartDateMap).sort(); |
|||
} |
|||
|
|||
for (const dateString of this.chartDates) { |
|||
if (dateString < startDateString) { |
|||
continue; |
|||
} else if (dateString > endDateString) { |
|||
break; |
|||
} |
|||
|
|||
if (ordersByDate[dateString]?.length > 0) { |
|||
for (const order of ordersByDate[dateString]) { |
|||
order.unitPriceFromMarketData = |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; |
|||
} |
|||
} else { |
|||
orders.push({ |
|||
date: dateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, |
|||
unitPriceFromMarketData: |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice |
|||
}); |
|||
} |
|||
|
|||
const lastOrder = orders.at(-1); |
|||
|
|||
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; |
|||
} |
|||
|
|||
// Sort orders so that the start and end placeholder order are at the correct
|
|||
// position
|
|||
orders = sortBy(orders, ({ date, itemType }) => { |
|||
let sortIndex = new Date(date); |
|||
|
|||
if (itemType === 'end') { |
|||
sortIndex = addMilliseconds(sortIndex, 1); |
|||
} else if (itemType === 'start') { |
|||
sortIndex = addMilliseconds(sortIndex, -1); |
|||
} |
|||
|
|||
return sortIndex.getTime(); |
|||
}); |
|||
|
|||
const indexOfStartOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'start'; |
|||
}); |
|||
|
|||
const indexOfEndOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'end'; |
|||
}); |
|||
|
|||
let totalInvestmentDays = 0; |
|||
let sumOfTimeWeightedInvestments = new Big(0); |
|||
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|||
|
|||
for (let i = 0; i < orders.length; i += 1) { |
|||
const order = orders[i]; |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log(); |
|||
console.log(); |
|||
console.log( |
|||
i + 1, |
|||
order.date, |
|||
order.type, |
|||
order.itemType ? `(${order.itemType})` : '' |
|||
); |
|||
} |
|||
|
|||
const exchangeRateAtOrderDate = exchangeRates[order.date]; |
|||
|
|||
if (order.type === 'DIVIDEND') { |
|||
const dividend = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalDividend = totalDividend.plus(dividend); |
|||
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( |
|||
dividend.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'INTEREST') { |
|||
const interest = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalInterest = totalInterest.plus(interest); |
|||
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( |
|||
interest.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'ITEM') { |
|||
const valuables = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalValuables = totalValuables.plus(valuables); |
|||
totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus( |
|||
valuables.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} else if (order.type === 'LIABILITY') { |
|||
const liabilities = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalLiabilities = totalLiabilities.plus(liabilities); |
|||
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( |
|||
liabilities.mul(exchangeRateAtOrderDate ?? 1) |
|||
); |
|||
} |
|||
|
|||
if (order.itemType === 'start') { |
|||
// Take the unit price of the order as the market price if there are no
|
|||
// orders of this symbol before the start date
|
|||
order.unitPrice = |
|||
indexOfStartOrder === 0 |
|||
? orders[i + 1]?.unitPrice |
|||
: unitPriceAtStartDate; |
|||
} |
|||
|
|||
if (order.fee) { |
|||
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); |
|||
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const unitPrice = ['BUY', 'SELL'].includes(order.type) |
|||
? order.unitPrice |
|||
: order.unitPriceFromMarketData; |
|||
|
|||
if (unitPrice) { |
|||
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); |
|||
|
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const valueOfInvestmentBeforeTransaction = totalUnits.mul( |
|||
order.unitPriceInBaseCurrency |
|||
); |
|||
|
|||
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); |
|||
|
|||
if (!investmentAtStartDate && i >= indexOfStartOrder) { |
|||
investmentAtStartDate = totalInvestment ?? new Big(0); |
|||
|
|||
investmentAtStartDateWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect ?? new Big(0); |
|||
|
|||
valueAtStartDate = valueOfInvestmentBeforeTransaction; |
|||
|
|||
valueAtStartDateWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} |
|||
|
|||
let transactionInvestment = new Big(0); |
|||
let transactionInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
if (order.type === 'BUY') { |
|||
transactionInvestment = order.quantity |
|||
.mul(order.unitPriceInBaseCurrency) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
transactionInvestmentWithCurrencyEffect = order.quantity |
|||
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
totalQuantityFromBuyTransactions = |
|||
totalQuantityFromBuyTransactions.plus(order.quantity); |
|||
|
|||
totalInvestmentFromBuyTransactions = |
|||
totalInvestmentFromBuyTransactions.plus(transactionInvestment); |
|||
|
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect = |
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (order.type === 'SELL') { |
|||
if (totalUnits.gt(0)) { |
|||
transactionInvestment = totalInvestment |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
transactionInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
} |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('order.quantity', order.quantity.toNumber()); |
|||
console.log('transactionInvestment', transactionInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'transactionInvestmentWithCurrencyEffect', |
|||
transactionInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const totalInvestmentBeforeTransaction = totalInvestment; |
|||
|
|||
const totalInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
totalInvestment = totalInvestment.plus(transactionInvestment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
|
|||
if (i >= indexOfStartOrder && !initialValue) { |
|||
if ( |
|||
i === indexOfStartOrder && |
|||
!valueOfInvestmentBeforeTransaction.eq(0) |
|||
) { |
|||
initialValue = valueOfInvestmentBeforeTransaction; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} else if (transactionInvestment.gt(0)) { |
|||
initialValue = transactionInvestment; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
transactionInvestmentWithCurrencyEffect; |
|||
} |
|||
} |
|||
|
|||
fees = fees.plus(order.feeInBaseCurrency ?? 0); |
|||
|
|||
feesWithCurrencyEffect = feesWithCurrencyEffect.plus( |
|||
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 |
|||
); |
|||
|
|||
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); |
|||
|
|||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); |
|||
|
|||
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( |
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
); |
|||
|
|||
const grossPerformanceFromSell = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrency |
|||
.minus(lastAveragePrice) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
const grossPerformanceFromSellWithCurrencyEffect = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
.minus(lastAveragePriceWithCurrencyEffect) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
grossPerformanceFromSells = grossPerformanceFromSells.plus( |
|||
grossPerformanceFromSell |
|||
); |
|||
|
|||
grossPerformanceFromSellsWithCurrencyEffect = |
|||
grossPerformanceFromSellsWithCurrencyEffect.plus( |
|||
grossPerformanceFromSellWithCurrencyEffect |
|||
); |
|||
|
|||
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactions.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( |
|||
0 |
|||
) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
'grossPerformanceFromSells', |
|||
grossPerformanceFromSells.toNumber() |
|||
); |
|||
console.log( |
|||
'grossPerformanceFromSellWithCurrencyEffect', |
|||
grossPerformanceFromSellWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const newGrossPerformance = valueOfInvestment |
|||
.minus(totalInvestment) |
|||
.plus(grossPerformanceFromSells); |
|||
|
|||
const newGrossPerformanceWithCurrencyEffect = |
|||
valueOfInvestmentWithCurrencyEffect |
|||
.minus(totalInvestmentWithCurrencyEffect) |
|||
.plus(grossPerformanceFromSellsWithCurrencyEffect); |
|||
|
|||
grossPerformance = newGrossPerformance; |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
newGrossPerformanceWithCurrencyEffect; |
|||
|
|||
if (order.itemType === 'start') { |
|||
feesAtStartDate = fees; |
|||
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; |
|||
grossPerformanceAtStartDate = grossPerformance; |
|||
|
|||
grossPerformanceAtStartDateWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect; |
|||
} |
|||
|
|||
if (i > indexOfStartOrder) { |
|||
// Only consider periods with an investment for the calculation of
|
|||
// the time weighted investment
|
|||
if ( |
|||
valueOfInvestmentBeforeTransaction.gt(0) && |
|||
['BUY', 'SELL'].includes(order.type) |
|||
) { |
|||
// Calculate the number of days since the previous order
|
|||
const orderDate = new Date(order.date); |
|||
const previousOrderDate = new Date(orders[i - 1].date); |
|||
|
|||
let daysSinceLastOrder = differenceInDays( |
|||
orderDate, |
|||
previousOrderDate |
|||
); |
|||
if (daysSinceLastOrder <= 0) { |
|||
// The time between two activities on the same day is unknown
|
|||
// -> Set it to the smallest floating point number greater than 0
|
|||
daysSinceLastOrder = Number.EPSILON; |
|||
} |
|||
|
|||
// Sum up the total investment days since the start date to calculate
|
|||
// the time weighted investment
|
|||
totalInvestmentDays += daysSinceLastOrder; |
|||
|
|||
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( |
|||
valueAtStartDate |
|||
.minus(investmentAtStartDate) |
|||
.plus(totalInvestmentBeforeTransaction) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
|
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect = |
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( |
|||
valueAtStartDateWithCurrencyEffect |
|||
.minus(investmentAtStartDateWithCurrencyEffect) |
|||
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
} |
|||
|
|||
currentValues[order.date] = valueOfInvestment; |
|||
|
|||
currentValuesWithCurrencyEffect[order.date] = |
|||
valueOfInvestmentWithCurrencyEffect; |
|||
|
|||
netPerformanceValues[order.date] = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
netPerformanceValuesWithCurrencyEffect[order.date] = |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.minus( |
|||
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) |
|||
); |
|||
|
|||
investmentValuesAccumulated[order.date] = totalInvestment; |
|||
|
|||
investmentValuesAccumulatedWithCurrencyEffect[order.date] = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
investmentValuesWithCurrencyEffect[order.date] = ( |
|||
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) |
|||
).add(transactionInvestmentWithCurrencyEffect); |
|||
|
|||
timeWeightedInvestmentValues[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('totalInvestment', totalInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'totalInvestmentWithCurrencyEffect', |
|||
totalInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformance', |
|||
grossPerformance.minus(grossPerformanceAtStartDate).toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformanceWithCurrencyEffect', |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.toNumber() |
|||
); |
|||
} |
|||
|
|||
if (i === indexOfEndOrder) { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const totalGrossPerformance = grossPerformance.minus( |
|||
grossPerformanceAtStartDate |
|||
); |
|||
|
|||
const totalGrossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.minus( |
|||
grossPerformanceAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
const totalNetPerformance = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDate = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalGrossPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentageWithCurrencyEffect = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( |
|||
0 |
|||
) |
|||
? totalGrossPerformanceWithCurrencyEffect.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnit = totalUnits.gt(0) |
|||
? fees.minus(feesAtStartDate).div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnitWithCurrencyEffect = totalUnits.gt(0) |
|||
? feesWithCurrencyEffect |
|||
.minus(feesAtStartDateWithCurrencyEffect) |
|||
.div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalNetPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentageWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
const netPerformanceWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
for (const dateRange of [ |
|||
'1d', |
|||
'1y', |
|||
'5y', |
|||
'max', |
|||
'mtd', |
|||
'wtd', |
|||
'ytd' |
|||
// TODO:
|
|||
// ...eachYearOfInterval({ end, start })
|
|||
// .filter((date) => {
|
|||
// return !isThisYear(date);
|
|||
// })
|
|||
// .map((date) => {
|
|||
// return format(date, 'yyyy');
|
|||
// })
|
|||
] as DateRange[]) { |
|||
const dateInterval = getIntervalFromDateRange(dateRange); |
|||
const endDate = dateInterval.endDate; |
|||
let startDate = dateInterval.startDate; |
|||
|
|||
if (isBefore(startDate, start)) { |
|||
startDate = start; |
|||
} |
|||
|
|||
const rangeEndDateString = format(endDate, DATE_FORMAT); |
|||
const rangeStartDateString = format(startDate, DATE_FORMAT); |
|||
|
|||
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|||
|
|||
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = |
|||
investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0); |
|||
|
|||
const grossPerformanceAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesAtDateRangeStartWithCurrencyEffect.minus( |
|||
investmentValuesAccumulatedAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
let average = new Big(0); |
|||
let dayCount = 0; |
|||
|
|||
for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { |
|||
const date = this.chartDates[i]; |
|||
|
|||
if (date > rangeEndDateString) { |
|||
continue; |
|||
} else if (date < rangeStartDateString) { |
|||
break; |
|||
} |
|||
|
|||
if ( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) |
|||
) { |
|||
average = average.add( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].add( |
|||
grossPerformanceAtDateRangeStartWithCurrencyEffect |
|||
) |
|||
); |
|||
|
|||
dayCount++; |
|||
} |
|||
} |
|||
|
|||
if (dayCount > 0) { |
|||
average = average.div(dayCount); |
|||
} |
|||
|
|||
netPerformanceWithCurrencyEffectMap[dateRange] = |
|||
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( |
|||
// If the date range is 'max', take 0 as a start value. Otherwise,
|
|||
// the value of the end of the day of the start date is taken which
|
|||
// differs from the buying price.
|
|||
dateRange === 'max' |
|||
? new Big(0) |
|||
: (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0)) |
|||
) ?? new Big(0); |
|||
|
|||
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) |
|||
? netPerformanceWithCurrencyEffectMap[dateRange].div(average) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
` |
|||
${symbol} |
|||
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( |
|||
2 |
|||
)} -> ${unitPriceAtEndDate.toFixed(2)} |
|||
Total investment: ${totalInvestment.toFixed(2)} |
|||
Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Total dividend: ${totalDividend.toFixed(2)} |
|||
Gross performance: ${totalGrossPerformance.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|||
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentageWithCurrencyEffect |
|||
.mul(100) |
|||
.toFixed(2)}% |
|||
Fees per unit: ${feesPerUnit.toFixed(2)} |
|||
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Net performance: ${totalNetPerformance.toFixed( |
|||
2 |
|||
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|||
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|||
'max' |
|||
].toFixed(2)}%` |
|||
); |
|||
} |
|||
|
|||
return { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
feesWithCurrencyEffect, |
|||
grossPerformancePercentage, |
|||
grossPerformancePercentageWithCurrencyEffect, |
|||
initialValue, |
|||
initialValueWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformancePercentage, |
|||
netPerformancePercentageWithCurrencyEffectMap, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
netPerformanceWithCurrencyEffectMap, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect, |
|||
totalAccountBalanceInBaseCurrency, |
|||
totalDividend, |
|||
totalDividendInBaseCurrency, |
|||
totalInterest, |
|||
totalInterestInBaseCurrency, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
totalLiabilities, |
|||
totalLiabilitiesInBaseCurrency, |
|||
totalValuables, |
|||
totalValuablesInBaseCurrency, |
|||
grossPerformance: totalGrossPerformance, |
|||
grossPerformanceWithCurrencyEffect: |
|||
totalGrossPerformanceWithCurrencyEffect, |
|||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), |
|||
netPerformance: totalNetPerformance, |
|||
timeWeightedInvestment: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate, |
|||
timeWeightedInvestmentWithCurrencyEffect: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
}; |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
} |
|||
|
@ -1,6 +0,0 @@ |
|||
import { IsString } from 'class-validator'; |
|||
|
|||
export class CreateTagDto { |
|||
@IsString() |
|||
name: string; |
|||
} |
@ -1,14 +0,0 @@ |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { TagController } from './tag.controller'; |
|||
import { TagService } from './tag.service'; |
|||
|
|||
@Module({ |
|||
controllers: [TagController], |
|||
exports: [TagService], |
|||
imports: [PrismaModule], |
|||
providers: [TagService] |
|||
}) |
|||
export class TagModule {} |
@ -1,81 +0,0 @@ |
|||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
|||
|
|||
import { Injectable } from '@nestjs/common'; |
|||
import { Prisma, Tag } from '@prisma/client'; |
|||
|
|||
@Injectable() |
|||
export class TagService { |
|||
public constructor(private readonly prismaService: PrismaService) {} |
|||
|
|||
public async createTag(data: Prisma.TagCreateInput) { |
|||
return this.prismaService.tag.create({ |
|||
data |
|||
}); |
|||
} |
|||
|
|||
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> { |
|||
return this.prismaService.tag.delete({ where }); |
|||
} |
|||
|
|||
public async getTag( |
|||
tagWhereUniqueInput: Prisma.TagWhereUniqueInput |
|||
): Promise<Tag> { |
|||
return this.prismaService.tag.findUnique({ |
|||
where: tagWhereUniqueInput |
|||
}); |
|||
} |
|||
|
|||
public async getTags({ |
|||
cursor, |
|||
orderBy, |
|||
skip, |
|||
take, |
|||
where |
|||
}: { |
|||
cursor?: Prisma.TagWhereUniqueInput; |
|||
orderBy?: Prisma.TagOrderByWithRelationInput; |
|||
skip?: number; |
|||
take?: number; |
|||
where?: Prisma.TagWhereInput; |
|||
} = {}) { |
|||
return this.prismaService.tag.findMany({ |
|||
cursor, |
|||
orderBy, |
|||
skip, |
|||
take, |
|||
where |
|||
}); |
|||
} |
|||
|
|||
public async getTagsWithActivityCount() { |
|||
const tagsWithOrderCount = await this.prismaService.tag.findMany({ |
|||
include: { |
|||
_count: { |
|||
select: { orders: true } |
|||
} |
|||
} |
|||
}); |
|||
|
|||
return tagsWithOrderCount.map(({ _count, id, name, userId }) => { |
|||
return { |
|||
id, |
|||
name, |
|||
userId, |
|||
activityCount: _count.orders |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public async updateTag({ |
|||
data, |
|||
where |
|||
}: { |
|||
data: Prisma.TagUpdateInput; |
|||
where: Prisma.TagWhereUniqueInput; |
|||
}): Promise<Tag> { |
|||
return this.prismaService.tag.update({ |
|||
data, |
|||
where |
|||
}); |
|||
} |
|||
} |
@ -1,9 +0,0 @@ |
|||
import { IsString } from 'class-validator'; |
|||
|
|||
export class UpdateTagDto { |
|||
@IsString() |
|||
id: string; |
|||
|
|||
@IsString() |
|||
name: string; |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,77 @@ |
|||
import { Rule } from '@ghostfolio/api/models/rule'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { UserSettings } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Settings } from './interfaces/rule-settings.interface'; |
|||
|
|||
export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> { |
|||
private asiaPacificValueInBaseCurrency: number; |
|||
private currentValueInBaseCurrency: number; |
|||
|
|||
public constructor( |
|||
protected exchangeRateDataService: ExchangeRateDataService, |
|||
currentValueInBaseCurrency: number, |
|||
asiaPacificValueInBaseCurrency: number |
|||
) { |
|||
super(exchangeRateDataService, { |
|||
key: RegionalMarketClusterRiskAsiaPacific.name, |
|||
name: 'Asia-Pacific' |
|||
}); |
|||
|
|||
this.asiaPacificValueInBaseCurrency = asiaPacificValueInBaseCurrency; |
|||
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
|||
} |
|||
|
|||
public evaluate(ruleSettings: Settings) { |
|||
const asiaPacificMarketValueRatio = this.currentValueInBaseCurrency |
|||
? this.asiaPacificValueInBaseCurrency / this.currentValueInBaseCurrency |
|||
: 0; |
|||
|
|||
if (asiaPacificMarketValueRatio > ruleSettings.thresholdMax) { |
|||
return { |
|||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
|||
ruleSettings.thresholdMax * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} else if (asiaPacificMarketValueRatio < ruleSettings.thresholdMin) { |
|||
return { |
|||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision( |
|||
3 |
|||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
|||
value: true |
|||
}; |
|||
} |
|||
|
|||
public getConfiguration() { |
|||
return { |
|||
threshold: { |
|||
max: 1, |
|||
min: 0, |
|||
step: 0.01, |
|||
unit: '%' |
|||
}, |
|||
thresholdMax: true, |
|||
thresholdMin: true |
|||
}; |
|||
} |
|||
|
|||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
|||
return { |
|||
baseCurrency, |
|||
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
|||
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03, |
|||
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02 |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,79 @@ |
|||
import { Rule } from '@ghostfolio/api/models/rule'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { UserSettings } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Settings } from './interfaces/rule-settings.interface'; |
|||
|
|||
export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> { |
|||
private currentValueInBaseCurrency: number; |
|||
private emergingMarketsValueInBaseCurrency: number; |
|||
|
|||
public constructor( |
|||
protected exchangeRateDataService: ExchangeRateDataService, |
|||
currentValueInBaseCurrency: number, |
|||
emergingMarketsValueInBaseCurrency: number |
|||
) { |
|||
super(exchangeRateDataService, { |
|||
key: RegionalMarketClusterRiskEmergingMarkets.name, |
|||
name: 'Emerging Markets' |
|||
}); |
|||
|
|||
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
|||
this.emergingMarketsValueInBaseCurrency = |
|||
emergingMarketsValueInBaseCurrency; |
|||
} |
|||
|
|||
public evaluate(ruleSettings: Settings) { |
|||
const emergingMarketsValueRatio = this.currentValueInBaseCurrency |
|||
? this.emergingMarketsValueInBaseCurrency / |
|||
this.currentValueInBaseCurrency |
|||
: 0; |
|||
|
|||
if (emergingMarketsValueRatio > ruleSettings.thresholdMax) { |
|||
return { |
|||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${( |
|||
ruleSettings.thresholdMax * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} else if (emergingMarketsValueRatio < ruleSettings.thresholdMin) { |
|||
return { |
|||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is below ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision( |
|||
3 |
|||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
|||
value: true |
|||
}; |
|||
} |
|||
|
|||
public getConfiguration() { |
|||
return { |
|||
threshold: { |
|||
max: 1, |
|||
min: 0, |
|||
step: 0.01, |
|||
unit: '%' |
|||
}, |
|||
thresholdMax: true, |
|||
thresholdMin: true |
|||
}; |
|||
} |
|||
|
|||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
|||
return { |
|||
baseCurrency, |
|||
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
|||
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.12, |
|||
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08 |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
import { Rule } from '@ghostfolio/api/models/rule'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { UserSettings } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Settings } from './interfaces/rule-settings.interface'; |
|||
|
|||
export class RegionalMarketClusterRiskEurope extends Rule<Settings> { |
|||
private currentValueInBaseCurrency: number; |
|||
private europeValueInBaseCurrency: number; |
|||
|
|||
public constructor( |
|||
protected exchangeRateDataService: ExchangeRateDataService, |
|||
currentValueInBaseCurrency: number, |
|||
europeValueInBaseCurrency: number |
|||
) { |
|||
super(exchangeRateDataService, { |
|||
key: RegionalMarketClusterRiskEurope.name, |
|||
name: 'Europe' |
|||
}); |
|||
|
|||
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
|||
this.europeValueInBaseCurrency = europeValueInBaseCurrency; |
|||
} |
|||
|
|||
public evaluate(ruleSettings: Settings) { |
|||
const europeMarketValueRatio = this.currentValueInBaseCurrency |
|||
? this.europeValueInBaseCurrency / this.currentValueInBaseCurrency |
|||
: 0; |
|||
|
|||
if (europeMarketValueRatio > ruleSettings.thresholdMax) { |
|||
return { |
|||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
|||
ruleSettings.thresholdMax * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} else if (europeMarketValueRatio < ruleSettings.thresholdMin) { |
|||
return { |
|||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision( |
|||
3 |
|||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
|||
value: true |
|||
}; |
|||
} |
|||
|
|||
public getConfiguration() { |
|||
return { |
|||
threshold: { |
|||
max: 1, |
|||
min: 0, |
|||
step: 0.01, |
|||
unit: '%' |
|||
}, |
|||
thresholdMax: true, |
|||
thresholdMin: true |
|||
}; |
|||
} |
|||
|
|||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
|||
return { |
|||
baseCurrency, |
|||
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
|||
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.15, |
|||
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.11 |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; |
|||
|
|||
export interface Settings extends RuleSettings { |
|||
baseCurrency: string; |
|||
thresholdMin: number; |
|||
thresholdMax: number; |
|||
} |
@ -0,0 +1,77 @@ |
|||
import { Rule } from '@ghostfolio/api/models/rule'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { UserSettings } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Settings } from './interfaces/rule-settings.interface'; |
|||
|
|||
export class RegionalMarketClusterRiskJapan extends Rule<Settings> { |
|||
private currentValueInBaseCurrency: number; |
|||
private japanValueInBaseCurrency: number; |
|||
|
|||
public constructor( |
|||
protected exchangeRateDataService: ExchangeRateDataService, |
|||
currentValueInBaseCurrency: number, |
|||
japanValueInBaseCurrency: number |
|||
) { |
|||
super(exchangeRateDataService, { |
|||
key: RegionalMarketClusterRiskJapan.name, |
|||
name: 'Japan' |
|||
}); |
|||
|
|||
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
|||
this.japanValueInBaseCurrency = japanValueInBaseCurrency; |
|||
} |
|||
|
|||
public evaluate(ruleSettings: Settings) { |
|||
const japanMarketValueRatio = this.currentValueInBaseCurrency |
|||
? this.japanValueInBaseCurrency / this.currentValueInBaseCurrency |
|||
: 0; |
|||
|
|||
if (japanMarketValueRatio > ruleSettings.thresholdMax) { |
|||
return { |
|||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
|||
ruleSettings.thresholdMax * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} else if (japanMarketValueRatio < ruleSettings.thresholdMin) { |
|||
return { |
|||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision( |
|||
3 |
|||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
|||
value: true |
|||
}; |
|||
} |
|||
|
|||
public getConfiguration() { |
|||
return { |
|||
threshold: { |
|||
max: 1, |
|||
min: 0, |
|||
step: 0.01, |
|||
unit: '%' |
|||
}, |
|||
thresholdMax: true, |
|||
thresholdMin: true |
|||
}; |
|||
} |
|||
|
|||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
|||
return { |
|||
baseCurrency, |
|||
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
|||
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.06, |
|||
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.04 |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
import { Rule } from '@ghostfolio/api/models/rule'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { UserSettings } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Settings } from './interfaces/rule-settings.interface'; |
|||
|
|||
export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> { |
|||
private currentValueInBaseCurrency: number; |
|||
private northAmericaValueInBaseCurrency: number; |
|||
|
|||
public constructor( |
|||
protected exchangeRateDataService: ExchangeRateDataService, |
|||
currentValueInBaseCurrency: number, |
|||
northAmericaValueInBaseCurrency: number |
|||
) { |
|||
super(exchangeRateDataService, { |
|||
key: RegionalMarketClusterRiskNorthAmerica.name, |
|||
name: 'North America' |
|||
}); |
|||
|
|||
this.currentValueInBaseCurrency = currentValueInBaseCurrency; |
|||
this.northAmericaValueInBaseCurrency = northAmericaValueInBaseCurrency; |
|||
} |
|||
|
|||
public evaluate(ruleSettings: Settings) { |
|||
const northAmericaMarketValueRatio = this.currentValueInBaseCurrency |
|||
? this.northAmericaValueInBaseCurrency / this.currentValueInBaseCurrency |
|||
: 0; |
|||
|
|||
if (northAmericaMarketValueRatio > ruleSettings.thresholdMax) { |
|||
return { |
|||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( |
|||
ruleSettings.thresholdMax * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} else if (northAmericaMarketValueRatio < ruleSettings.thresholdMin) { |
|||
return { |
|||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) is below ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision(3)}%`,
|
|||
value: false |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( |
|||
ruleSettings.thresholdMin * 100 |
|||
).toPrecision( |
|||
3 |
|||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
|||
value: true |
|||
}; |
|||
} |
|||
|
|||
public getConfiguration() { |
|||
return { |
|||
threshold: { |
|||
max: 1, |
|||
min: 0, |
|||
step: 0.01, |
|||
unit: '%' |
|||
}, |
|||
thresholdMax: true, |
|||
thresholdMin: true |
|||
}; |
|||
} |
|||
|
|||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { |
|||
return { |
|||
baseCurrency, |
|||
isActive: xRayRules?.[this.getKey()]?.isActive ?? true, |
|||
thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.69, |
|||
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.65 |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { BenchmarkService } from './benchmark.service'; |
|||
|
|||
@Module({ |
|||
exports: [BenchmarkService], |
|||
imports: [ |
|||
DataProviderModule, |
|||
MarketDataModule, |
|||
PrismaModule, |
|||
PropertyModule, |
|||
RedisCacheModule, |
|||
SymbolProfileModule |
|||
], |
|||
providers: [BenchmarkService] |
|||
}) |
|||
export class BenchmarkModule {} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue