mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
				 26 changed files with 706 additions and 130 deletions
			
			
		@ -0,0 +1,146 @@ | 
				
			|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; | 
				
			|||
import { | 
				
			|||
  activityDummyData, | 
				
			|||
  symbolProfileDummyData, | 
				
			|||
  userDummyData | 
				
			|||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; | 
				
			|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; | 
				
			|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; | 
				
			|||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; | 
				
			|||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; | 
				
			|||
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; | 
				
			|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; | 
				
			|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; | 
				
			|||
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; | 
				
			|||
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; | 
				
			|||
import { parseDate } from '@ghostfolio/common/helper'; | 
				
			|||
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; | 
				
			|||
 | 
				
			|||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { | 
				
			|||
  return { | 
				
			|||
    CurrentRateService: jest.fn().mockImplementation(() => { | 
				
			|||
      return CurrentRateServiceMock; | 
				
			|||
    }) | 
				
			|||
  }; | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
jest.mock( | 
				
			|||
  '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', | 
				
			|||
  () => { | 
				
			|||
    return { | 
				
			|||
      // eslint-disable-next-line @typescript-eslint/naming-convention
 | 
				
			|||
      PortfolioSnapshotService: jest.fn().mockImplementation(() => { | 
				
			|||
        return PortfolioSnapshotServiceMock; | 
				
			|||
      }) | 
				
			|||
    }; | 
				
			|||
  } | 
				
			|||
); | 
				
			|||
 | 
				
			|||
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { | 
				
			|||
  return { | 
				
			|||
    // eslint-disable-next-line @typescript-eslint/naming-convention
 | 
				
			|||
    RedisCacheService: jest.fn().mockImplementation(() => { | 
				
			|||
      return RedisCacheServiceMock; | 
				
			|||
    }) | 
				
			|||
  }; | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
describe('PortfolioCalculator', () => { | 
				
			|||
  let configurationService: ConfigurationService; | 
				
			|||
  let currentRateService: CurrentRateService; | 
				
			|||
  let exchangeRateDataService: ExchangeRateDataService; | 
				
			|||
  let portfolioCalculatorFactory: PortfolioCalculatorFactory; | 
				
			|||
  let portfolioSnapshotService: PortfolioSnapshotService; | 
				
			|||
  let redisCacheService: RedisCacheService; | 
				
			|||
 | 
				
			|||
  beforeEach(() => { | 
				
			|||
    configurationService = new ConfigurationService(); | 
				
			|||
    currentRateService = new CurrentRateService(null, null, null, null); | 
				
			|||
    exchangeRateDataService = new ExchangeRateDataService( | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null | 
				
			|||
    ); | 
				
			|||
    portfolioSnapshotService = new PortfolioSnapshotService(null); | 
				
			|||
    redisCacheService = new RedisCacheService(null, null); | 
				
			|||
    portfolioCalculatorFactory = new PortfolioCalculatorFactory( | 
				
			|||
      configurationService, | 
				
			|||
      currentRateService, | 
				
			|||
      exchangeRateDataService, | 
				
			|||
      portfolioSnapshotService, | 
				
			|||
      redisCacheService | 
				
			|||
    ); | 
				
			|||
  }); | 
				
			|||
 | 
				
			|||
  describe('get transaction point', () => { | 
				
			|||
    it('with MSFT buy and sell with fractional quantities (multiples of 1/3)', () => { | 
				
			|||
      jest.useFakeTimers().setSystemTime(parseDate('2024-04-01').getTime()); | 
				
			|||
 | 
				
			|||
      const activities: Activity[] = [ | 
				
			|||
        { | 
				
			|||
          ...activityDummyData, | 
				
			|||
          date: new Date('2024-03-08'), | 
				
			|||
          feeInAssetProfileCurrency: 0, | 
				
			|||
          quantity: 0.3333333333333333, | 
				
			|||
          SymbolProfile: { | 
				
			|||
            ...symbolProfileDummyData, | 
				
			|||
            currency: 'USD', | 
				
			|||
            dataSource: 'YAHOO', | 
				
			|||
            name: 'Microsoft Inc.', | 
				
			|||
            symbol: 'MSFT' | 
				
			|||
          }, | 
				
			|||
          type: 'BUY', | 
				
			|||
          unitPriceInAssetProfileCurrency: 408 | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          ...activityDummyData, | 
				
			|||
          date: new Date('2024-03-13'), | 
				
			|||
          quantity: 0.6666666666666666, | 
				
			|||
          feeInAssetProfileCurrency: 0, | 
				
			|||
          SymbolProfile: { | 
				
			|||
            ...symbolProfileDummyData, | 
				
			|||
            currency: 'USD', | 
				
			|||
            dataSource: 'YAHOO', | 
				
			|||
            name: 'Microsoft Inc.', | 
				
			|||
            symbol: 'MSFT' | 
				
			|||
          }, | 
				
			|||
          type: 'BUY', | 
				
			|||
          unitPriceInAssetProfileCurrency: 400 | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          ...activityDummyData, | 
				
			|||
          date: new Date('2024-03-14'), | 
				
			|||
          quantity: 1, | 
				
			|||
          feeInAssetProfileCurrency: 0, | 
				
			|||
          SymbolProfile: { | 
				
			|||
            ...symbolProfileDummyData, | 
				
			|||
            currency: 'USD', | 
				
			|||
            dataSource: 'YAHOO', | 
				
			|||
            name: 'Microsoft Inc.', | 
				
			|||
            symbol: 'MSFT' | 
				
			|||
          }, | 
				
			|||
          type: 'SELL', | 
				
			|||
          unitPriceInAssetProfileCurrency: 411 | 
				
			|||
        } | 
				
			|||
      ]; | 
				
			|||
 | 
				
			|||
      const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ | 
				
			|||
        activities, | 
				
			|||
        calculationType: PerformanceCalculationType.ROAI, | 
				
			|||
        currency: 'USD', | 
				
			|||
        userId: userDummyData.id | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      const transactionPoints = portfolioCalculator.getTransactionPoints(); | 
				
			|||
      const lastTransactionPoint = | 
				
			|||
        transactionPoints[transactionPoints.length - 1]; | 
				
			|||
      const position = lastTransactionPoint.items.find( | 
				
			|||
        (item) => item.symbol === 'MSFT' | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      expect(position.investment.toNumber()).toBe(0); | 
				
			|||
      expect(position.quantity.toNumber()).toBe(0); | 
				
			|||
    }); | 
				
			|||
  }); | 
				
			|||
}); | 
				
			|||
@ -0,0 +1,17 @@ | 
				
			|||
import { publicRoutes } from '@ghostfolio/common/routes/routes'; | 
				
			|||
 | 
				
			|||
import { Component } from '@angular/core'; | 
				
			|||
import { MatButtonModule } from '@angular/material/button'; | 
				
			|||
import { RouterModule } from '@angular/router'; | 
				
			|||
 | 
				
			|||
@Component({ | 
				
			|||
  host: { class: 'page' }, | 
				
			|||
  imports: [MatButtonModule, RouterModule], | 
				
			|||
  selector: 'gf-hacktoberfest-2025-page', | 
				
			|||
  templateUrl: './hacktoberfest-2025-page.html' | 
				
			|||
}) | 
				
			|||
export class Hacktoberfest2025PageComponent { | 
				
			|||
  public routerLinkAbout = publicRoutes.about.routerLink; | 
				
			|||
  public routerLinkBlog = publicRoutes.blog.routerLink; | 
				
			|||
  public routerLinkOpenStartup = publicRoutes.openStartup.routerLink; | 
				
			|||
} | 
				
			|||
@ -0,0 +1,201 @@ | 
				
			|||
<div class="blog container"> | 
				
			|||
  <div class="row"> | 
				
			|||
    <div class="col-md-8 offset-md-2"> | 
				
			|||
      <article> | 
				
			|||
        <div class="mb-4 text-center"> | 
				
			|||
          <h1 class="mb-1">Hacktoberfest 2025</h1> | 
				
			|||
          <div class="mb-3 text-muted"><small>2025-09-27</small></div> | 
				
			|||
          <img | 
				
			|||
            alt="Hacktoberfest 2025 with Ghostfolio Teaser" | 
				
			|||
            class="rounded w-100" | 
				
			|||
            src="../assets/images/blog/hacktoberfest-2025.png" | 
				
			|||
            title="Hacktoberfest 2025 with Ghostfolio" | 
				
			|||
          /> | 
				
			|||
        </div> | 
				
			|||
        <section class="mb-4"> | 
				
			|||
          <p> | 
				
			|||
            Ghostfolio is joining | 
				
			|||
            <a href="https://hacktoberfest.com">Hacktoberfest</a> for the fourth | 
				
			|||
            time and <a [routerLink]="routerLinkAbout">we</a> are looking | 
				
			|||
            forward to meeting new open-source contributors along the way. Every | 
				
			|||
            year in October, Hacktoberfest celebrates open source by | 
				
			|||
            highlighting projects, maintainers, and contributors from around the | 
				
			|||
            globe. Open source maintainers dedicate extra time to support new | 
				
			|||
            contributors while guiding them through their first pull requests on | 
				
			|||
            <a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. | 
				
			|||
          </p> | 
				
			|||
        </section> | 
				
			|||
        <section class="mb-4"> | 
				
			|||
          <h2 class="h4"> | 
				
			|||
            Meet Ghostfolio: a modern Dashboard for Personal Finance | 
				
			|||
          </h2> | 
				
			|||
          <p> | 
				
			|||
            <a href="https://ghostfol.io">Ghostfolio</a> is a web application | 
				
			|||
            that makes it easy to manage your personal finances. It aggregates | 
				
			|||
            your assets and helps you make informed decisions to balance your | 
				
			|||
            portfolio or plan future investments. | 
				
			|||
          </p> | 
				
			|||
          <p> | 
				
			|||
            The software is fully written in | 
				
			|||
            <a href="https://www.typescriptlang.org">TypeScript</a> and | 
				
			|||
            organized as an <a href="https://nx.dev">Nx</a> workspace, utilizing | 
				
			|||
            the latest framework releases. The backend is based on | 
				
			|||
            <a href="https://nestjs.com">NestJS</a> in combination with | 
				
			|||
            <a href="https://www.postgresql.org">PostgreSQL</a> as a database | 
				
			|||
            together with <a href="https://www.prisma.io">Prisma</a> and | 
				
			|||
            <a href="https://redis.io">Redis</a> for caching. The frontend is | 
				
			|||
            developed with <a href="https://angular.dev">Angular</a>. | 
				
			|||
          </p> | 
				
			|||
          <p> | 
				
			|||
            With over 200 contributors, the OSS project is used daily by a | 
				
			|||
            growing global community. Ghostfolio counts more than | 
				
			|||
            <a [routerLink]="routerLinkOpenStartup">6’500 stars on GitHub</a> | 
				
			|||
            and | 
				
			|||
            <a [routerLink]="routerLinkOpenStartup" | 
				
			|||
              >1’600’000+ pulls on Docker Hub</a | 
				
			|||
            >, standing out for its simple and user-friendly experience. | 
				
			|||
          </p> | 
				
			|||
        </section> | 
				
			|||
        <section class="mb-4"> | 
				
			|||
          <h2 class="h4">How you can make an impact</h2> | 
				
			|||
          <p> | 
				
			|||
            Every contribution makes a difference. Whether it is implementing | 
				
			|||
            new features, resolving bugs, refactoring code, enhancing | 
				
			|||
            documentation, adding unit tests, or translating content into | 
				
			|||
            another language, you can actively shape our project. | 
				
			|||
          </p> | 
				
			|||
          <p> | 
				
			|||
            New to our codebase? No worries! We have labeled a few | 
				
			|||
            <a | 
				
			|||
              href="https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest" | 
				
			|||
              >issues</a | 
				
			|||
            > | 
				
			|||
            with <code>hacktoberfest</code> that are ideal for newcomers. | 
				
			|||
          </p> | 
				
			|||
          <p> | 
				
			|||
            The official Hacktoberfest website provides some valuable | 
				
			|||
            <a | 
				
			|||
              href="https://hacktoberfest.com/participation/#beginner-resources" | 
				
			|||
              >resources for beginners</a | 
				
			|||
            > | 
				
			|||
            to start contributing in open source. | 
				
			|||
          </p> | 
				
			|||
        </section> | 
				
			|||
        <section class="mb-4"> | 
				
			|||
          <h2 class="h4">Connect with us</h2> | 
				
			|||
          <p> | 
				
			|||
            If you have further questions or ideas, please join our | 
				
			|||
            <a | 
				
			|||
              href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" | 
				
			|||
              >Slack</a | 
				
			|||
            > | 
				
			|||
            community or get in touch on X | 
				
			|||
            <a href="https://x.com/ghostfolio_">@ghostfolio_</a>. | 
				
			|||
          </p> | 
				
			|||
          <p> | 
				
			|||
            We look forward to collaborating.<br /> | 
				
			|||
            Thomas from Ghostfolio | 
				
			|||
          </p> | 
				
			|||
        </section> | 
				
			|||
        <section class="mb-4"> | 
				
			|||
          <ul class="list-inline"> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Angular</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Community</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Dashboard</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Docker</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Finance</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Fintech</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Ghostfolio</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">GitHub</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Hacktoberfest</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Hacktoberfest 2025</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Investment</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">NestJS</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Nx</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">October</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Open Source</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">OSS</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Personal Finance</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Portfolio</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Portfolio Tracker</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Prisma</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Redis</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Software</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">TypeScript</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">UX</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Wealth</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Wealth Management</span> | 
				
			|||
            </li> | 
				
			|||
            <li class="list-inline-item"> | 
				
			|||
              <span class="badge badge-light">Web Application</span> | 
				
			|||
            </li> | 
				
			|||
          </ul> | 
				
			|||
        </section> | 
				
			|||
        <nav aria-label="breadcrumb"> | 
				
			|||
          <ol class="breadcrumb"> | 
				
			|||
            <li class="breadcrumb-item"> | 
				
			|||
              <a i18n [routerLink]="routerLinkBlog">Blog</a> | 
				
			|||
            </li> | 
				
			|||
            <li | 
				
			|||
              aria-current="page" | 
				
			|||
              class="active breadcrumb-item text-truncate" | 
				
			|||
            > | 
				
			|||
              Hacktoberfest 2025 | 
				
			|||
            </li> | 
				
			|||
          </ol> | 
				
			|||
        </nav> | 
				
			|||
      </article> | 
				
			|||
    </div> | 
				
			|||
  </div> | 
				
			|||
</div> | 
				
			|||
@ -0,0 +1,8 @@ | 
				
			|||
import { Type } from '@prisma/client'; | 
				
			|||
 | 
				
			|||
export const ActivityType = { | 
				
			|||
  ...Type, | 
				
			|||
  VALUABLE: 'VALUABLE' | 
				
			|||
} as const; | 
				
			|||
 | 
				
			|||
export type ActivityType = (typeof ActivityType)[keyof typeof ActivityType]; | 
				
			|||
| 
		 After Width: | Height: | Size: 123 KiB  | 
@ -0,0 +1,8 @@ | 
				
			|||
-- AlterEnum | 
				
			|||
BEGIN; | 
				
			|||
CREATE TYPE "public"."Type_new" AS ENUM ('BUY', 'DIVIDEND', 'FEE', 'INTEREST', 'LIABILITY', 'SELL'); | 
				
			|||
ALTER TABLE "public"."Order" ALTER COLUMN "type" TYPE "public"."Type_new" USING ("type"::text::"public"."Type_new"); | 
				
			|||
ALTER TYPE "public"."Type" RENAME TO "Type_old"; | 
				
			|||
ALTER TYPE "public"."Type_new" RENAME TO "Type"; | 
				
			|||
DROP TYPE "public"."Type_old"; | 
				
			|||
COMMIT; | 
				
			|||
@ -1,20 +0,0 @@ | 
				
			|||
{ | 
				
			|||
  "meta": { | 
				
			|||
    "date": "2023-02-05T00:00:00.000Z", | 
				
			|||
    "version": "dev" | 
				
			|||
  }, | 
				
			|||
  "activities": [ | 
				
			|||
    { | 
				
			|||
      "accountId": null, | 
				
			|||
      "comment": null, | 
				
			|||
      "fee": 0, | 
				
			|||
      "quantity": 1, | 
				
			|||
      "type": "ITEM", | 
				
			|||
      "unitPrice": 500000, | 
				
			|||
      "currency": "USD", | 
				
			|||
      "dataSource": "MANUAL", | 
				
			|||
      "date": "2022-01-01T00:00:00.000Z", | 
				
			|||
      "symbol": "Penthouse Apartment" | 
				
			|||
    } | 
				
			|||
  ] | 
				
			|||
} | 
				
			|||
					Loading…
					
					
				
		Reference in new issue