feat: trading feedback engine — periodic performance reports with AI summarization
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
- Migration 038: trading_reports table + report-summarizer agent seed
- 6 reporting modules: models, collector, sections, validator, summarizer, generator
- API endpoints: GET /api/reports (paginated, filterable), GET /api/reports/{id}
- Frontend hooks: useReports, useReport with TanStack Query
- Scheduler: daily (after 16:30 ET) and weekly (Saturday) report triggers
- Redis queue consumer for async report generation with retry/dedup
- 5 property-based tests (chunking, serialization, validation, accuracy, deltas)
- 109 unit/integration tests across all modules
- 6 frontend hook tests with MSW mocks
This commit is contained in:
@@ -1051,3 +1051,44 @@ export function useValidationAttributionLayers(lookback = '30d', horizon = '7d')
|
||||
const path = `/api/validation/attribution/layers${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<LayerAttributionResponse>(['validation-attribution-layers', lookback, horizon], 'query', path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trading Reports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ReportListItem {
|
||||
id: string;
|
||||
report_type: string;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
validation_status: string;
|
||||
generated_at: string;
|
||||
}
|
||||
|
||||
export interface ReportDetail extends ReportListItem {
|
||||
report_data: Record<string, unknown>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function useReports(params?: {
|
||||
report_type?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.report_type) qs.set('report_type', params.report_type);
|
||||
if (params?.start_date) qs.set('start_date', params.start_date);
|
||||
if (params?.end_date) qs.set('end_date', params.end_date);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.offset) qs.set('offset', String(params.offset));
|
||||
const path = `/api/reports${qs.toString() ? '?' + qs : ''}`;
|
||||
return useGet<ReportListItem[]>(['reports', params], 'query', path);
|
||||
}
|
||||
|
||||
export function useReport(id: string | undefined) {
|
||||
return useGet<ReportDetail>(
|
||||
['report', id], 'query', `/api/reports/${id}`, !!id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -334,6 +334,17 @@ export const handlers = [
|
||||
return HttpResponse.json({ enabled: body.enabled, previous_enabled: true, toggled_by: 'operator' });
|
||||
}),
|
||||
|
||||
// Trading Reports
|
||||
http.get('/api/reports', () => HttpResponse.json([
|
||||
{ id: 'rpt-1', report_type: 'daily', period_start: '2025-01-15', period_end: '2025-01-15', validation_status: 'passed', generated_at: '2025-01-15T21:30:00Z' },
|
||||
])),
|
||||
http.get('/api/reports/:id', ({ params }) => {
|
||||
if (params.id === 'rpt-1') {
|
||||
return HttpResponse.json({ id: 'rpt-1', report_type: 'daily', period_start: '2025-01-15', period_end: '2025-01-15', report_data: { pnl: { realized_pnl: 125.5 }, executive_summary: 'Test' }, validation_status: 'passed', generated_at: '2025-01-15T21:30:00Z', created_at: '2025-01-15T21:30:05Z' });
|
||||
}
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
}),
|
||||
|
||||
// Validation: Model Quality & Calibration endpoints
|
||||
http.get('/api/validation/summary', () => HttpResponse.json(mockValidationSummary)),
|
||||
http.get('/api/validation/calibration', () => HttpResponse.json(mockValidationCalibration)),
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Frontend hook tests for trading reports.
|
||||
*
|
||||
* Tests useReports and useReport hooks with MSW mocks.
|
||||
* Requirements validated: 5.4, 5.5
|
||||
*/
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { type ReactNode, createElement } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { useReports, useReport } from '../api/hooks';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
const mockReportList = [
|
||||
{
|
||||
id: 'rpt-1',
|
||||
report_type: 'daily',
|
||||
period_start: '2025-01-15',
|
||||
period_end: '2025-01-15',
|
||||
validation_status: 'passed',
|
||||
generated_at: '2025-01-15T21:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 'rpt-2',
|
||||
report_type: 'weekly',
|
||||
period_start: '2025-01-13',
|
||||
period_end: '2025-01-17',
|
||||
validation_status: 'warnings',
|
||||
generated_at: '2025-01-18T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
const mockReportDetail = {
|
||||
id: 'rpt-1',
|
||||
report_type: 'daily',
|
||||
period_start: '2025-01-15',
|
||||
period_end: '2025-01-15',
|
||||
validation_status: 'passed',
|
||||
generated_at: '2025-01-15T21:30:00Z',
|
||||
created_at: '2025-01-15T21:30:05Z',
|
||||
report_data: {
|
||||
pnl: { realized_pnl: 125.5, unrealized_pnl: -30.2 },
|
||||
executive_summary: 'Test executive summary',
|
||||
},
|
||||
};
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
},
|
||||
});
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
}
|
||||
|
||||
describe('useReports', () => {
|
||||
it('fetches report list with default params', async () => {
|
||||
server.use(
|
||||
http.get('/api/reports', () => HttpResponse.json(mockReportList)),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useReports(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toHaveLength(2);
|
||||
expect(result.current.data![0].id).toBe('rpt-1');
|
||||
expect(result.current.data![0].report_type).toBe('daily');
|
||||
expect(result.current.data![1].report_type).toBe('weekly');
|
||||
});
|
||||
|
||||
it('passes query params for filtering', async () => {
|
||||
let capturedUrl = '';
|
||||
server.use(
|
||||
http.get('/api/reports', ({ request }) => {
|
||||
capturedUrl = request.url;
|
||||
return HttpResponse.json([mockReportList[0]]);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useReports({ report_type: 'daily', limit: 10 }),
|
||||
{ wrapper: createWrapper() },
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(capturedUrl).toContain('report_type=daily');
|
||||
expect(capturedUrl).toContain('limit=10');
|
||||
expect(result.current.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles error state', async () => {
|
||||
server.use(
|
||||
http.get('/api/reports', () =>
|
||||
new HttpResponse(null, { status: 500 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useReports(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReport', () => {
|
||||
it('fetches single report by id', async () => {
|
||||
server.use(
|
||||
http.get('/api/reports/rpt-1', () =>
|
||||
HttpResponse.json(mockReportDetail),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useReport('rpt-1'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data!.id).toBe('rpt-1');
|
||||
expect(result.current.data!.report_data).toBeDefined();
|
||||
expect(result.current.data!.report_data.pnl).toBeDefined();
|
||||
expect(result.current.data!.created_at).toBe('2025-01-15T21:30:05Z');
|
||||
});
|
||||
|
||||
it('does not fetch when id is undefined', async () => {
|
||||
const { result } = renderHook(() => useReport(undefined), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
// Should stay in idle/loading state without fetching
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('handles 404 error', async () => {
|
||||
server.use(
|
||||
http.get('/api/reports/nonexistent', () =>
|
||||
new HttpResponse(null, { status: 404 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useReport('nonexistent'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user