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

- 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:
Celes Renata
2026-05-01 22:13:09 +00:00
parent 376fcb4bb4
commit bc077bfcc8
28 changed files with 6771 additions and 1 deletions
+109
View File
@@ -4107,3 +4107,112 @@ async def get_validation_attribution_layers(
"lookback": lookback,
"horizon": horizon,
}
# ---------------------------------------------------------------------------
# Trading Reports
# ---------------------------------------------------------------------------
@app.get("/api/reports")
async def list_reports(
report_type: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: int = Query(default=20, le=100),
offset: int = Query(default=0, ge=0),
):
"""Paginated list of trading reports with optional filtering.
Query params:
- report_type: 'daily' or 'weekly'
- start_date: ISO date (YYYY-MM-DD) — filter period_start >= this
- end_date: ISO date (YYYY-MM-DD) — filter period_end <= this
- limit: max results (default 20, max 100)
- offset: pagination offset (default 0)
Requirements: 5.4, 5.5, 5.6
"""
conditions: list[str] = []
params: list[Any] = []
idx = 1
if report_type:
if report_type not in ("daily", "weekly"):
raise HTTPException(400, "report_type must be 'daily' or 'weekly'")
conditions.append(f"report_type = ${idx}")
params.append(report_type)
idx += 1
if start_date:
try:
from datetime import date as _date
_date.fromisoformat(start_date)
except ValueError:
raise HTTPException(400, "start_date must be YYYY-MM-DD")
conditions.append(f"period_start >= ${idx}::date")
params.append(start_date)
idx += 1
if end_date:
try:
from datetime import date as _date
_date.fromisoformat(end_date)
except ValueError:
raise HTTPException(400, "end_date must be YYYY-MM-DD")
conditions.append(f"period_end <= ${idx}::date")
params.append(end_date)
idx += 1
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
query = f"""
SELECT id, report_type, period_start, period_end,
validation_status, generated_at
FROM trading_reports
{where}
ORDER BY generated_at DESC
LIMIT ${idx} OFFSET ${idx + 1}
"""
params.extend([limit, offset])
rows = await pool.fetch(query, *params)
return [
{
"id": str(r["id"]),
"report_type": r["report_type"],
"period_start": r["period_start"].isoformat(),
"period_end": r["period_end"].isoformat(),
"validation_status": r["validation_status"],
"generated_at": r["generated_at"].isoformat(),
}
for r in rows
]
@app.get("/api/reports/{report_id}")
async def get_report(report_id: str):
"""Fetch a single report including full report_data JSONB.
Requirements: 5.4, 5.5
"""
row = await pool.fetchrow(
"""SELECT id, report_type, period_start, period_end,
report_data, validation_status, generated_at, created_at
FROM trading_reports
WHERE id = $1::uuid""",
report_id,
)
if row is None:
raise HTTPException(404, "Report not found")
return {
"id": str(row["id"]),
"report_type": row["report_type"],
"period_start": row["period_start"].isoformat(),
"period_end": row["period_end"].isoformat(),
"report_data": json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"],
"validation_status": row["validation_status"],
"generated_at": row["generated_at"].isoformat(),
"created_at": row["created_at"].isoformat(),
}