Browse Source

Add MCP tool to get performance

pull/7994/head
Thomas Kaul 2 weeks ago
parent
commit
43d4ddf8aa
  1. 2
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  2. 52
      apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts
  3. 53
      apps/api/src/services/portfolio-table/portfolio-table.service.ts

2
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -79,7 +79,7 @@ export class GhostfolioMcpController {
}, },
description: `Gives the performance of the portfolio in the date range with these columns: ${PortfolioTableService.getPerformanceTableColumnNames().join( description: `Gives the performance of the portfolio in the date range with these columns: ${PortfolioTableService.getPerformanceTableColumnNames().join(
', ' ', '
)}. The asset performance excludes the effect of the exchange rates, the currency performance is that effect, and the net performance is the sum of both in the base currency of the user. The parameters limit the performance to the holdings of the accounts, of the asset classes or of the asset profile.`, )}. The asset performance excludes the effect of the exchange rates, the currency performance is that effect, and the net performance is the sum of both in the base currency of the user. Each performance is the return on average investment (ROAI) and excludes the dividends. The accounts and the activities which are excluded from analysis are not part of the performance. The parameters limit the performance to the holdings of the accounts, of the asset classes or of the asset profile.`,
name: 'get-performance', name: 'get-performance',
parameters: GET_PERFORMANCE_PARAMETERS parameters: GET_PERFORMANCE_PARAMETERS
}) })

52
apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts

@ -7,6 +7,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
HistoricalDataItem,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
WatchlistResponse WatchlistResponse
@ -157,12 +158,14 @@ function createWatchlistItem({
function createPortfolioTableService({ function createPortfolioTableService({
accounts = [], accounts = [],
chart = [{ date: '2024-01-01' }],
errors = [], errors = [],
holdings = [], holdings = [],
performance = createPerformance(), performance = createPerformance(),
watchlist = [] watchlist = []
}: { }: {
accounts?: AccountWithValue[]; accounts?: AccountWithValue[];
chart?: HistoricalDataItem[];
errors?: PortfolioPerformanceResponse['errors']; errors?: PortfolioPerformanceResponse['errors'];
holdings?: PortfolioPosition[]; holdings?: PortfolioPosition[];
performance?: PortfolioPerformanceResponse['performance']; performance?: PortfolioPerformanceResponse['performance'];
@ -179,9 +182,12 @@ function createPortfolioTableService({
const portfolioService = { const portfolioService = {
getAccountsWithAggregations: jest.fn().mockResolvedValue({ accounts }), getAccountsWithAggregations: jest.fn().mockResolvedValue({ accounts }),
getDetails: jest.fn().mockResolvedValue({ holdings }), getDetails: jest.fn().mockResolvedValue({ holdings }),
getPerformance: jest getPerformance: jest.fn().mockResolvedValue({
.fn() chart,
.mockResolvedValue({ errors, performance, hasErrors: errors.length > 0 }) errors,
performance,
hasErrors: errors.length > 0
})
} as unknown as PortfolioService; } as unknown as PortfolioService;
const watchlistService = { const watchlistService = {
@ -382,23 +388,44 @@ describe('PortfolioTableService', () => {
expect(row).toBe('| 10.000% | 5.000% | 15.000% |'); expect(row).toBe('| 10.000% | 5.000% | 15.000% |');
}); });
it('gives a currency performance of zero without a sign', async () => {
const result = await getPerformanceTable({
performance: createPerformance({
netPerformancePercentage: 0.10000000000000003,
netPerformancePercentageWithCurrencyEffect: 0.1
})
});
const [row] = result.split('\n').filter((line) => {
return line.startsWith('| 10.000%');
});
expect(row).toBe('| 10.000% | 0.000% | 10.000% |');
});
it('gives no monetary value', async () => { it('gives no monetary value', async () => {
const result = await getPerformanceTable(); const result = await getPerformanceTable();
for (const value of ['50', '200', '300', '1700', '2000', '3000']) { const [, , row] = result.split('\n').filter((line) => {
expect(result).not.toContain(value); return line.startsWith('|');
});
for (const cell of row.split('|').slice(1, -1)) {
expect(cell.trim()).toMatch(/^-?\d+\.\d{3}%$/);
} }
}); });
it('tells the symbols of which the market data is delayed', async () => { it('tells the asset profiles of which the market data is delayed', async () => {
const result = await getPerformanceTable({ const result = await getPerformanceTable({
errors: [ errors: [
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }, { dataSource: DataSource.MANUAL, symbol: 'AAPL' },
{ dataSource: DataSource.YAHOO, symbol: 'MSFT' } { dataSource: DataSource.YAHOO, symbol: 'AAPL' }
] ]
}); });
expect(result).toContain('Market data is delayed for: AAPL, MSFT'); expect(result).toContain(
'Market data is delayed for: AAPL (MANUAL), AAPL (YAHOO)'
);
}); });
it('tells no delay of the market data if there is no error', async () => { it('tells no delay of the market data if there is no error', async () => {
@ -406,6 +433,13 @@ describe('PortfolioTableService', () => {
expect(result).not.toContain('Market data is delayed'); expect(result).not.toContain('Market data is delayed');
}); });
it('tells that no performance is found if the chart is empty', async () => {
const result = await getPerformanceTable({ chart: [] });
expect(result).toContain('No performance found.');
expect(result).not.toContain('%');
});
}); });
describe('getWatchlistTable', () => { describe('getWatchlistTable', () => {

53
apps/api/src/services/portfolio-table/portfolio-table.service.ts

@ -201,10 +201,14 @@ export class PortfolioTableService {
netPerformancePercentage, netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect netPerformancePercentageWithCurrencyEffect
}) => { }) => {
return getPercentage( const currencyPerformancePercentage = getPercentage(
netPerformancePercentageWithCurrencyEffect - netPerformancePercentageWithCurrencyEffect -
netPerformancePercentage netPerformancePercentage
); );
return Number.parseFloat(currencyPerformancePercentage) === 0
? getPercentage(0)
: currencyPerformancePercentage;
}, },
name: 'Currency Performance in Percentage' name: 'Currency Performance in Percentage'
}, },
@ -454,31 +458,36 @@ export class PortfolioTableService {
filters?: Filter[]; filters?: Filter[];
userId: string; userId: string;
}) { }) {
const { errors, performance } = await this.portfolioService.getPerformance({ const { chart, errors, performance } =
dateRange, await this.portfolioService.getPerformance({
filters, dateRange,
userId filters,
}); userId
});
const performanceSection = [ const performanceSection = ['## Performance', ''];
'## Performance',
'',
await getMarkdownTable({
columnDefinitions:
PortfolioTableService.PERFORMANCE_TABLE_COLUMN_DEFINITIONS,
rows: [performance]
})
];
if (errors?.length > 0) { if (chart?.length > 0) {
performanceSection.push( performanceSection.push(
'', await getMarkdownTable({
`Market data is delayed for: ${errors columnDefinitions:
.map(({ symbol }) => { PortfolioTableService.PERFORMANCE_TABLE_COLUMN_DEFINITIONS,
return symbol; rows: [performance]
}) })
.join(', ')}`
); );
if (errors?.length > 0) {
performanceSection.push(
'',
`Market data is delayed for: ${errors
.map(({ dataSource, symbol }) => {
return `${symbol} (${dataSource})`;
})
.join(', ')}`
);
}
} else {
performanceSection.push('No performance found.');
} }
return performanceSection.join('\n'); return performanceSection.join('\n');

Loading…
Cancel
Save