Browse Source

renaming annualizedDividendYield

pull/6258/head
Sven Günther 4 days ago
parent
commit
86b5bef849
  1. 119
      CLAUDE.md
  2. 33
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  4. 33
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  5. 23
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  6. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  7. 6
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  8. 4
      apps/api/src/app/portfolio/portfolio.service.ts
  9. 10
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  10. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  11. 2
      libs/common/src/lib/models/portfolio-snapshot.ts
  12. 2
      libs/common/src/lib/models/timeline-position.ts

119
CLAUDE.md

@ -0,0 +1,119 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Ghostfolio is an open source wealth management software built with TypeScript in an Nx monorepo workspace. It's a full-stack application with Angular frontend and NestJS backend, using PostgreSQL with Prisma ORM and Redis for caching.
## Development Commands
### Environment Setup
```bash
npm install
docker compose -f docker/docker-compose.dev.yml up -d # Start PostgreSQL and Redis
npm run database:setup # Initialize database schema
```
### Development Servers
```bash
npm run start:server # Start NestJS API server
npm run start:client # Start Angular client (English)
npm run watch:server # Start server in watch mode for debugging
```
### Build and Production
```bash
npm run build:production # Build both API and client for production
npm run start:production # Run production build with database migration
```
### Database Operations
```bash
npm run database:push # Sync schema with database (development)
npm run database:migrate # Apply migrations (production)
npm run database:seed # Seed database with initial data
npm run database:gui # Open Prisma Studio
npm run database:format-schema # Format Prisma schema
npm run database:generate-typings # Generate Prisma client
```
### Testing and Quality
```bash
npm test # Run all tests (API + common)
npm run test:api # Run API tests only
npm run test:common # Run common library tests
npm run test:single # Run single test file (example provided)
npm run lint # Run ESLint on all projects
npm run format # Format code with Prettier
npm run format:check # Check code formatting
```
### Nx Workspace Commands
```bash
nx affected:build # Build affected projects
nx affected:test # Test affected projects
nx affected:lint # Lint affected projects
nx dep-graph # View dependency graph
```
### Storybook (Component Library)
```bash
npm run start:storybook # Start Storybook development server
npm run build:storybook # Build Storybook for production
```
## Architecture
### Monorepo Structure
- **apps/api**: NestJS backend application
- **apps/client**: Angular frontend application
- **apps/client-e2e**: E2E tests for client
- **apps/ui-e2e**: E2E tests for UI components
- **libs/common**: Shared TypeScript libraries and utilities
- **libs/ui**: Angular UI component library
### Technology Stack
- **Frontend**: Angular 20 with Angular Material, Bootstrap utility classes
- **Backend**: NestJS with TypeScript
- **Database**: PostgreSQL with Prisma ORM
- **Caching**: Redis with Bull for job queues
- **Build Tool**: Nx workspace
- **Testing**: Jest for unit tests, Cypress for E2E tests
### Key Dependencies
- **Authentication**: Passport (JWT, Google OAuth, WebAuthn)
- **Data Sources**: Yahoo Finance, CoinGecko APIs for market data
- **Charts**: Chart.js with various plugins
- **Payment**: Stripe integration
- **Internationalization**: Angular i18n with multiple language support
### Database Schema
The Prisma schema defines models for:
- User management and access control
- Account and portfolio tracking
- Trading activities and orders
- Market data and asset information
- Platform integrations
### Development Notes
- Node.js version >=22.18.0 required
- Uses Nx generators for consistent code scaffolding
- Husky for git hooks and code quality enforcement
- Environment files: `.env.dev` for development, `.env.example` as template
- SSL certificates can be generated for localhost development
- Experimental features can be toggled via user settings
- always run the .husky pre commit hooks after code changes

33
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -119,6 +119,7 @@ export abstract class PortfolioCalculator {
this.activities = activities
.map(
({
currency,
date,
feeInAssetProfileCurrency,
feeInBaseCurrency,
@ -139,6 +140,7 @@ export abstract class PortfolioCalculator {
}
return {
currency,
SymbolProfile,
tags,
type,
@ -186,7 +188,7 @@ export abstract class PortfolioCalculator {
if (!transactionPoints.length) {
return {
activitiesCount: 0,
annualizedDividendYield: 0,
dividendYieldTrailingTwelveMonths: 0,
createdAt: new Date(),
currentValueInBaseCurrency: new Big(0),
errors: [],
@ -405,38 +407,43 @@ export abstract class PortfolioCalculator {
};
}
// Calculate annualized dividend yield based on investment (cost basis)
// Calculate dividend yield based on trailing twelve months of dividends and investment (cost basis)
const twelveMonthsAgo = subYears(this.endDate, 1);
const dividendsLast12Months = this.activities
.filter(({ SymbolProfile, type, date }) => {
return (
SymbolProfile.symbol === item.symbol &&
type === 'DIVIDEND' &&
new Date(date) >= twelveMonthsAgo &&
new Date(date) <= this.endDate
isWithinInterval(new Date(date), {
start: twelveMonthsAgo,
end: this.endDate
})
);
})
.reduce((sum, activity) => {
const activityCurrency =
activity.currency ?? activity.SymbolProfile.currency;
const exchangeRate =
exchangeRatesByCurrency[
`${activity.SymbolProfile.currency}${this.currency}`
]?.[format(new Date(activity.date), DATE_FORMAT)] ?? 1;
exchangeRatesByCurrency[`${activityCurrency}${this.currency}`]?.[
format(new Date(activity.date), DATE_FORMAT)
] ?? 1;
const dividendAmount = activity.quantity.mul(activity.unitPrice);
return sum.plus(dividendAmount.mul(exchangeRate));
}, new Big(0));
const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0)
? dividendsLast12Months
.div(totalInvestmentWithCurrencyEffect)
.toNumber()
: 0;
const dividendYieldTrailingTwelveMonths =
totalInvestmentWithCurrencyEffect.gt(0)
? dividendsLast12Months
.div(totalInvestmentWithCurrencyEffect)
.toNumber()
: 0;
positions.push({
includeInTotalAssetValue,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
activitiesCount: item.activitiesCount,
annualizedDividendYield,
dividendYieldTrailingTwelveMonths,
averagePrice: item.averagePrice,
currency: item.currency,
dataSource: item.dataSource,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -231,7 +231,7 @@ describe('PortfolioCalculator', () => {
*/
expect(position).toMatchObject<TimelinePosition>({
activitiesCount: 2,
annualizedDividendYield: 0,
dividendYieldTrailingTwelveMonths: 0,
averagePrice: new Big(1),
currency: 'USD',
dataSource: DataSource.YAHOO,

33
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -296,8 +296,8 @@ describe('PortfolioCalculator', () => {
});
const position = portfolioSnapshot.positions[0];
expect(position).toHaveProperty('annualizedDividendYield');
expect(position.annualizedDividendYield).toBeGreaterThan(0);
expect(position).toHaveProperty('dividendYieldTrailingTwelveMonths');
expect(position.dividendYieldTrailingTwelveMonths).toBeGreaterThan(0);
// Verify that the snapshot data is sufficient for portfolio summary calculation
// Portfolio summary annualized dividend yield = totalDividend / totalInvestment
@ -305,7 +305,7 @@ describe('PortfolioCalculator', () => {
.div(position.investmentWithCurrencyEffect)
.toNumber();
expect(position.annualizedDividendYield).toBeCloseTo(
expect(position.dividendYieldTrailingTwelveMonths).toBeCloseTo(
expectedPortfolioYield,
10
);
@ -501,12 +501,18 @@ describe('PortfolioCalculator', () => {
// MSFT: 2.60 dividends / 300 investment = 0.00867 (0.867%)
expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('2.6'));
expect(msftPosition.investmentWithCurrencyEffect).toEqual(new Big('300'));
expect(msftPosition.annualizedDividendYield).toBeCloseTo(2.6 / 300, 5);
expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo(
2.6 / 300,
5
);
// IBM: 6.60 dividends / 200 investment = 0.033 (3.3%)
expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('6.6'));
expect(ibmPosition.investmentWithCurrencyEffect).toEqual(new Big('200'));
expect(ibmPosition.annualizedDividendYield).toBeCloseTo(6.6 / 200, 5);
expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo(
6.6 / 200,
5
);
// Portfolio-wide: (2.60 + 6.60) / (300 + 200) = 9.20 / 500 = 0.0184 (1.84%)
const totalDividends = new Big(msftPosition.dividendInBaseCurrency).plus(
@ -519,9 +525,14 @@ describe('PortfolioCalculator', () => {
expect(totalDividends.toNumber()).toBe(9.2);
expect(totalInvestment.toNumber()).toBe(500);
// Test that portfolioSnapshot has aggregated annualizedDividendYield
expect(portfolioSnapshot).toHaveProperty('annualizedDividendYield');
expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo(0.0184, 4);
// Test that portfolioSnapshot has aggregated dividendYieldTrailingTwelveMonths
expect(portfolioSnapshot).toHaveProperty(
'dividendYieldTrailingTwelveMonths'
);
expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo(
0.0184,
4
);
});
it('ignores dividends older than 12 months when aggregating portfolio yield', async () => {
@ -667,11 +678,11 @@ describe('PortfolioCalculator', () => {
const ibmDividendLast12Months = new Big('1.65');
const totalInvestment = new Big('500');
expect(msftPosition.annualizedDividendYield).toBeCloseTo(
expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo(
msftDividendLast12Months.div(new Big('300')).toNumber(),
6
);
expect(ibmPosition.annualizedDividendYield).toBeCloseTo(
expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo(
ibmDividendLast12Months.div(new Big('200')).toNumber(),
6
);
@ -681,7 +692,7 @@ describe('PortfolioCalculator', () => {
.div(totalInvestment)
.toNumber();
expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo(
expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo(
expectedAnnualizedDividendYield,
6
);

23
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -34,7 +34,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
let netPerformance = new Big(0);
let totalDividendsLast12MonthsInBaseCurrency = new Big(0);
let totalDividendsTrailingTwelveMonthsInBaseCurrency = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0);
const totalInterestWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0);
@ -48,9 +48,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
)) {
if (currentPosition.investmentWithCurrencyEffect) {
totalDividendsLast12MonthsInBaseCurrency =
totalDividendsLast12MonthsInBaseCurrency.plus(
new Big(currentPosition.annualizedDividendYield ?? 0).mul(
totalDividendsTrailingTwelveMonthsInBaseCurrency =
totalDividendsTrailingTwelveMonthsInBaseCurrency.plus(
new Big(currentPosition.dividendYieldTrailingTwelveMonths ?? 0).mul(
currentPosition.investmentWithCurrencyEffect
)
);
@ -115,12 +115,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
}
// Calculate annualized dividend yield for the entire portfolio
const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0)
? totalDividendsLast12MonthsInBaseCurrency
.div(totalInvestmentWithCurrencyEffect)
.toNumber()
: 0;
// Calculate dividend yield for the entire portfolio based on trailing twelve months
const dividendYieldTrailingTwelveMonths =
totalInvestmentWithCurrencyEffect.gt(0)
? totalDividendsTrailingTwelveMonthsInBaseCurrency
.div(totalInvestmentWithCurrencyEffect)
.toNumber()
: 0;
return {
currentValueInBaseCurrency,
@ -133,7 +134,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
activitiesCount: this.activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
annualizedDividendYield,
dividendYieldTrailingTwelveMonths,
createdAt: new Date(),
errors: [],
historicalData: [],

1
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts

@ -1,6 +1,7 @@
import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
currency?: string;
date: string;
fee: Big;
feeInBaseCurrency: Big;

6
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -24,7 +24,7 @@ describe('PortfolioService', () => {
jest.restoreAllMocks();
});
it('returns annualizedDividendYield from the calculator snapshot', async () => {
it('returns dividendYieldTrailingTwelveMonths from the calculator snapshot', async () => {
const activities: Activity[] = [
{
...activityDummyData,
@ -111,7 +111,7 @@ describe('PortfolioService', () => {
getInterestInBaseCurrency: jest.fn().mockResolvedValue(new Big(1)),
getLiabilitiesInBaseCurrency: jest.fn().mockResolvedValue(new Big(6)),
getSnapshot: jest.fn().mockResolvedValue({
annualizedDividendYield: 0.0123,
dividendYieldTrailingTwelveMonths: 0.0123,
currentValueInBaseCurrency: new Big(500),
totalInvestment: new Big(400)
}),
@ -155,7 +155,7 @@ describe('PortfolioService', () => {
expect(portfolioCalculator.getSnapshot).toHaveBeenCalledTimes(1);
expect(summary).toMatchObject({
annualizedDividendYield: 0.0123,
dividendYieldTrailingTwelveMonths: 0.0123,
cash: 1000,
committedFunds: 60,
dividendInBaseCurrency: 12,

4
apps/api/src/app/portfolio/portfolio.service.ts

@ -1861,7 +1861,7 @@ export class PortfolioService {
}
const {
annualizedDividendYield,
dividendYieldTrailingTwelveMonths,
currentValueInBaseCurrency,
totalInvestment
} = await portfolioCalculator.getSnapshot();
@ -1962,7 +1962,7 @@ export class PortfolioService {
})?.toNumber();
return {
annualizedDividendYield,
dividendYieldTrailingTwelveMonths,
annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash,

10
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -376,8 +376,10 @@
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 ml-3 text-truncate" i18n>
Annualized Dividend Yield
<div class="flex-grow-1 ml-3 text-truncate">
<ng-container i18n>Dividend Yield</ng-container> (<ng-container i18n
>Trailing Twelve Months</ng-container
>)
</div>
<div class="flex-column flex-wrap justify-content-end">
<gf-value
@ -386,7 +388,9 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : summary?.annualizedDividendYield"
[value]="
isLoading ? undefined : summary?.dividendYieldTrailingTwelveMonths
"
/>
</div>
</div>

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -3,7 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance {
activityCount: number;
annualizedDividendYield: number;
dividendYieldTrailingTwelveMonths: number;
annualizedPerformancePercent: number;
annualizedPerformancePercentWithCurrencyEffect: number;
cash: number;

2
libs/common/src/lib/models/portfolio-snapshot.ts

@ -10,7 +10,7 @@ import { Transform, Type } from 'class-transformer';
export class PortfolioSnapshot {
activitiesCount: number;
annualizedDividendYield: number;
dividendYieldTrailingTwelveMonths: number;
createdAt: Date;

2
libs/common/src/lib/models/timeline-position.ts

@ -10,7 +10,7 @@ import { Transform, Type } from 'class-transformer';
export class TimelinePosition {
activitiesCount: number;
annualizedDividendYield: number;
dividendYieldTrailingTwelveMonths: number;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)

Loading…
Cancel
Save