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(
', '
)}. 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',
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
} from '@ghostfolio/common/config';
import {
HistoricalDataItem,
PortfolioPerformanceResponse,
PortfolioPosition,
WatchlistResponse
@ -157,12 +158,14 @@ function createWatchlistItem({
function createPortfolioTableService({
accounts = [],
chart = [{ date: '2024-01-01' }],
errors = [],
holdings = [],
performance = createPerformance(),
watchlist = []
}: {
accounts?: AccountWithValue[];
chart?: HistoricalDataItem[];
errors?: PortfolioPerformanceResponse['errors'];
holdings?: PortfolioPosition[];
performance?: PortfolioPerformanceResponse['performance'];
@ -179,9 +182,12 @@ function createPortfolioTableService({
const portfolioService = {
getAccountsWithAggregations: jest.fn().mockResolvedValue({ accounts }),
getDetails: jest.fn().mockResolvedValue({ holdings }),
getPerformance: jest
.fn()
.mockResolvedValue({ errors, performance, hasErrors: errors.length > 0 })
getPerformance: jest.fn().mockResolvedValue({
chart,
errors,
performance,
hasErrors: errors.length > 0
})
} as unknown as PortfolioService;
const watchlistService = {
@ -382,23 +388,44 @@ describe('PortfolioTableService', () => {
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 () => {
const result = await getPerformanceTable();
for (const value of ['50', '200', '300', '1700', '2000', '3000']) {
expect(result).not.toContain(value);
const [, , row] = result.split('\n').filter((line) => {
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({
errors: [
{ dataSource: DataSource.YAHOO, symbol: 'AAPL' },
{ dataSource: DataSource.YAHOO, symbol: 'MSFT' }
{ dataSource: DataSource.MANUAL, symbol: 'AAPL' },
{ 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 () => {
@ -406,6 +433,13 @@ describe('PortfolioTableService', () => {
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', () => {

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

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

Loading…
Cancel
Save