diff --git a/CHANGELOG.md b/CHANGELOG.md index 895421caf5..721c285e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Extended the content of the _General_ section by information about the import of activities from _Interactive Brokers_ on the Frequently Asked Questions (FAQ) page + +### Changed + +- Improved the _Storybook_ stories of the account selector, accounts table, activities table and holdings table components +- Improved the language localization for Spanish (`es`) + ### Fixed +- Fixed the missing list numbers in the top and bottom holdings on the analysis page in Safari +- Fixed the missing first data point in the portfolio evolution chart on the analysis page +- Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset +- Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service - Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles ## 3.81.0 - 2026-10-07 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 7a8bbcc741..ca440c6d2a 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1407,7 +1407,7 @@ export abstract class PortfolioCalculator { return this.snapshot.totalLiabilitiesWithCurrencyEffect; } - public async getPerformance({ end, start }) { + public async getPerformance({ end, start }: { end: Date; start: Date }) { await this.snapshotPromise; const { historicalData } = this.snapshot; @@ -1418,14 +1418,13 @@ export abstract class PortfolioCalculator { let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; - const startDate = isEqual(start, endOfDay(start)) - ? resetHours(start) - : start; + const endDate = this.toChartDate(end); + const startDate = this.toChartDate(start); for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); - if (!isBefore(date, startDate) && !isAfter(date, end)) { + if (!isBefore(date, startDate) && !isAfter(date, endDate)) { // Take the values at the start date from the first day of the date // range if (historicalDataItemsOfDateRange.length === 0) { @@ -1915,4 +1914,8 @@ export abstract class PortfolioCalculator { await this.initialize(attempt + 1); } } + + private toChartDate(aDate: Date) { + return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate; + } } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts index 5f8d9cdc44..43ff160a78 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts @@ -1,3 +1,7 @@ +/** + * @jest-environment /jest-environment-tz.js + * @jest-environment-options {"timeZone": "America/New_York"} + */ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -9,6 +13,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con 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 { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; @@ -183,4 +188,43 @@ describe('PortfolioCalculator', () => { ).toBeCloseTo(0.23, 10); }); }); + + describe('get performance', () => { + it('with a calendar year date range', async () => { + const snapshot = await portfolioCalculator.getSnapshot(); + + snapshot.historicalData = [ + getHistoricalDataItem({ + date: '2016-12-31', + investment: 100, + netPerformance: 0 + }), + getHistoricalDataItem({ + date: '2017-12-31', + investment: 100, + netPerformance: 10 + }), + getHistoricalDataItem({ + date: '2018-01-01', + investment: 100, + netPerformance: 20 + }) + ]; + + const { endDate, startDate } = getIntervalFromDateRange({ + dateRange: '2017' + }); + + const { chart } = await portfolioCalculator.getPerformance({ + end: endDate, + start: startDate + }); + + expect( + chart.map(({ date }) => { + return date; + }) + ).toEqual(['2016-12-31', '2017-12-31']); + }); + }); }); diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index d3b72ab349..e6ad639b84 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -58,6 +58,7 @@ export class FinancialModelingPrepService 'Congo (Dem. Rep. of the)': 'CD', 'Congo (Rep. of)': 'CG', 'Czech Republic': 'CZ', + Korea: 'KR', 'Korea (the Republic of)': 'KR', Macau: 'MO', 'Russian Federation': 'RU', diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index 618626b63e..14fe4d228f 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -6,123 +6,131 @@ (ngSubmit)="onSubmit()" >
- @if ( - data.rule.configuration?.thresholdMin && - data.rule.configuration?.thresholdMax - ) { -
-
- Threshold range: - - - - -
-
- - - - - - + @if (data.rule.configuration?.threshold; as threshold) { + @if ( + data.rule.configuration.thresholdMin && + data.rule.configuration.thresholdMax + ) { +
+
+ Threshold range: + + - + +
+
+ + + + + + +
-
- } @else { -
-
- Threshold Min: - -
-
- - - - - + } @else { +
+
+ Threshold Min: + +
+
+ + + + + +
-
-
-
- Threshold Max: - -
-
- - - - - +
+
+ Threshold Max: + +
+
+ + + + + +
-
+ } }
diff --git a/apps/client/src/app/pages/faq/overview/faq-overview-page.html b/apps/client/src/app/pages/faq/overview/faq-overview-page.html index 66b416fbab..cca488898e 100644 --- a/apps/client/src/app/pages/faq/overview/faq-overview-page.html +++ b/apps/client/src/app/pages/faq/overview/faq-overview-page.html @@ -56,6 +56,65 @@ return). + + + How can I import activities from + Interactive Brokers (IBKR)? + + +
    +
  1. Log in to your Interactive Brokers Client Portal
  2. +
  3. + Navigate to Performance & Reports in the top + menu, then select Flex Queries +
  4. +
  5. + Click the + button next to Activity Flex Query +
  6. +
  7. Select Trades under Sections
  8. +
  9. Select Executions on the pop-up window
  10. +
  11. + Click SELECT ALL and then Save +
  12. +
  13. Enter a Query Name (e.g., "Ghostfolio Export")
  14. +
  15. + Under Delivery Configuration, set Format to + CSV, + Include header and trailer records to + No, Include column headers to + Yes and + Display single column header row to + Yes +
  16. +
  17. Click Create
  18. +
  19. Run the Activity Flex Query
  20. +
  21. + Under Period, select Custom Date Range and choose + the dates you want to export. Run separate exports for longer + histories if necessary +
  22. +
  23. + Set Format to CSV, click + Run and download your trades +
  24. +
  25. + Import the CSV file in Ghostfolio: Portfolio → + Activities → + Import Activities... +
  26. +
+

+ Please note: The export contains trades only. It does not contain + dividends, interest or taxes. Keep only rows with the asset class + STK. Remove all other rows before the import, for + example currency conversions (CASH, such as + EUR.USD). Symbols from exchanges outside the US might + need a manual change. +

+
+
Can I use Ghostfolio anonymously? diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 5629965006..62debb78a5 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -329,36 +329,30 @@ export class GfAnalysisPageComponent implements OnInit { this.performanceDataItems = []; this.performanceDataItemsInPercentage = []; - for (const [ - index, - { - date, - dividendInPercentageWithCurrencyEffect, - netPerformanceInPercentageWithCurrencyEffect, - totalInvestmentValueWithCurrencyEffect, - valueInPercentage, - valueWithCurrencyEffect + for (const { + date, + dividendInPercentageWithCurrencyEffect, + netPerformanceInPercentageWithCurrencyEffect, + totalInvestmentValueWithCurrencyEffect, + valueInPercentage, + valueWithCurrencyEffect + } of chart ?? []) { + if (totalInvestmentValueWithCurrencyEffect !== undefined) { + this.investments.push({ + date, + investment: totalInvestmentValueWithCurrencyEffect + }); } - ] of (chart ?? []).entries()) { - // Ignore first item where value is 0 - if (index > 0 || this.user?.settings?.dateRange === 'max') { - if (totalInvestmentValueWithCurrencyEffect !== undefined) { - this.investments.push({ - date, - investment: totalInvestmentValueWithCurrencyEffect - }); - } - const value = isNumber(valueWithCurrencyEffect) - ? valueWithCurrencyEffect - : valueInPercentage; + const value = isNumber(valueWithCurrencyEffect) + ? valueWithCurrencyEffect + : valueInPercentage; - if (isNumber(value)) { - this.performanceDataItems.push({ - date, - value - }); - } + if (isNumber(value)) { + this.performanceDataItems.push({ + date, + value + }); } if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) { diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 96dfe24c45..c39b774f4b 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -330,7 +330,7 @@ > -
    +
      @for (holding of top3; track holding) {
    1. -
      - {{ holding.assetProfile.name }} +
      + + {{ holding.assetProfile.name }} +
      -
        +
          @for (holding of bottom3; track holding) {
        1. -
          - {{ holding.assetProfile.name }} +
          + + {{ holding.assetProfile.name }} +