Browse Source

Feature/integrate Fuse.js in GET holdings endpoint (#5062)

* Integrate Fuse.js in GET holdings endpoint

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/5060/head
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
d9fb159c6a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 6
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 189
      apps/api/src/app/portfolio/portfolio.service.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Introduced fuzzy search for the holdings of the assistant
- Introduced fuzzy search for the quick links of the assistant
- Improved the search results of the assistant to only display categories with content
- Enhanced the sitemap to dynamically compose public routes

6
apps/api/src/app/portfolio/portfolio.controller.ts

@ -412,19 +412,19 @@ export class PortfolioController {
filterByAssetClasses,
filterByDataSource,
filterByHoldingType,
filterBySearchQuery,
filterBySymbol,
filterByTags
});
const { holdings } = await this.portfolioService.getDetails({
const holdings = await this.portfolioService.getHoldings({
dateRange,
filters,
impersonationId,
query: filterBySearchQuery,
userId: this.request.user.id
});
return { holdings: Object.values(holdings) };
return { holdings };
}
@Get('investments')

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

@ -49,7 +49,6 @@ import {
PortfolioPosition,
PortfolioReportResponse,
PortfolioSummary,
Position,
UserSettings
} from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
@ -92,6 +91,8 @@ import { PortfolioCalculator } from './calculator/portfolio-calculator';
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
import { RulesService } from './rules.service';
const Fuse = require('fuse.js');
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
const developedMarkets = require('../../assets/countries/developed-markets.json');
const emergingMarkets = require('../../assets/countries/emerging-markets.json');
@ -269,6 +270,43 @@ export class PortfolioService {
return dividends;
}
public async getHoldings({
dateRange,
filters,
impersonationId,
query,
userId
}: {
dateRange: DateRange;
filters?: Filter[];
impersonationId: string;
query?: string;
userId: string;
}) {
userId = await this.getUserId(impersonationId, userId);
const { holdings: holdingsMap } = await this.getDetails({
dateRange,
filters,
impersonationId,
userId
});
let holdings = Object.values(holdingsMap);
if (query) {
const fuse = new Fuse(holdings, {
keys: ['isin', 'name', 'symbol'],
threshold: 0.3
});
holdings = fuse.search(query).map(({ item }) => {
return item;
});
}
return holdings;
}
public async getInvestments({
dateRange,
filters,
@ -977,155 +1015,6 @@ export class PortfolioService {
}
}
public async getHoldings({
dateRange = 'max',
filters,
impersonationId
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
}): Promise<{ hasErrors: boolean; positions: Position[] }> {
const searchQuery = filters.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities } =
await this.orderService.getOrdersForPortfolioCalculator({
filters,
userCurrency,
userId
});
if (activities.length === 0) {
return {
hasErrors: false,
positions: []
};
}
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
filters,
userId,
calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency
});
const portfolioSnapshot = await portfolioCalculator.getSnapshot();
const hasErrors = portfolioSnapshot.hasErrors;
let positions = portfolioSnapshot.positions;
positions = positions.filter(({ quantity }) => {
return !quantity.eq(0);
});
const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.getQuotes({
user,
items: assetProfileIdentifiers
}),
this.symbolProfileService.getSymbolProfiles(
positions.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
})
)
]);
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
for (const symbolProfile of symbolProfiles) {
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
}
if (searchQuery) {
positions = positions.filter(({ symbol }) => {
const enhancedSymbolProfile = symbolProfileMap[symbol];
return (
enhancedSymbolProfile.isin?.toLowerCase().startsWith(searchQuery) ||
enhancedSymbolProfile.name?.toLowerCase().startsWith(searchQuery) ||
enhancedSymbolProfile.symbol?.toLowerCase().startsWith(searchQuery)
);
});
}
return {
hasErrors,
positions: positions.map(
({
averagePrice,
currency,
dataSource,
firstBuyDate,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
investment,
investmentWithCurrencyEffect,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffectMap,
netPerformanceWithCurrencyEffectMap,
quantity,
symbol,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
transactionCount
}) => {
return {
currency,
dataSource,
firstBuyDate,
symbol,
transactionCount,
assetClass: symbolProfileMap[symbol].assetClass,
assetSubClass: symbolProfileMap[symbol].assetSubClass,
averagePrice: averagePrice.toNumber(),
grossPerformance: grossPerformance?.toNumber() ?? null,
grossPerformancePercentage:
grossPerformancePercentage?.toNumber() ?? null,
grossPerformancePercentageWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? null,
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect?.toNumber() ?? null,
investment: investment.toNumber(),
investmentWithCurrencyEffect:
investmentWithCurrencyEffect?.toNumber(),
marketState:
dataProviderResponses[symbol]?.marketState ?? 'delayed',
name: symbolProfileMap[symbol].name,
netPerformance: netPerformance?.toNumber() ?? null,
netPerformancePercentage:
netPerformancePercentage?.toNumber() ?? null,
netPerformancePercentageWithCurrencyEffect:
netPerformancePercentageWithCurrencyEffectMap?.[
dateRange
]?.toNumber() ?? null,
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ??
null,
quantity: quantity.toNumber(),
timeWeightedInvestment: timeWeightedInvestment?.toNumber(),
timeWeightedInvestmentWithCurrencyEffect:
timeWeightedInvestmentWithCurrencyEffect?.toNumber()
};
}
)
};
}
public async getPerformance({
dateRange = 'max',
filters,

Loading…
Cancel
Save