diff --git a/TECH_DEBT_REGISTER.md b/TECH_DEBT_REGISTER.md index a402fc3e..86e86155 100644 --- a/TECH_DEBT_REGISTER.md +++ b/TECH_DEBT_REGISTER.md @@ -59,6 +59,7 @@ | DEBT-024 | New integration tests don't insert FK parent rows / one pure-logic test flakes under full-suite run | Low (1) | Low (1) | Backlog | `TradeExecutionTests` constructs `Trade` with a random `sellDecisionId` that was never inserted into `sell_decisions`, so every insert now correctly fails its FK constraint (`trades_sell_decision_id_fkey`) once the schema was actually complete (see DEBT-020) — test-only gap, not a production code defect; needs the tests updated to insert a parent `models`+`sell_decisions` row first. Separately, `SellPriorityRankerTests.CalculateScore_HardImpairment_ReturnsLowestScore` (pure logic, no DB) passed in isolation but returned 1000 instead of the expected 950 (age-boost not applied) when run as part of the full suite — not yet root-caused; may be test-order/parallelization state leakage rather than a `SellPriorityRanker` bug. Also, `DbUpMigrationTests.*` (pre-existing, unrelated to this session) fail locally with `42501: must be owner of database kartsell_migration_test` — a local Postgres role permission gap, not a code issue. | @claude | Session 2026-08-07 (deploy failure triage) | | DEBT-025 | `Features/ApprovalWorkflow` has no `GET /approvals/{id}` endpoint | Medium (2) | Low (1) | Completed (DB verification pending) | Added `GetApprovalByIdEndpoint` (`GET /approvals/{id}`) + `ApprovalDetailResponse` (includes `Evidence`), and `ApprovalWorkflowSql.GetEvidenceForProposalAsync`. Evidence attached during approval (PBO/DSR/OOS artifact links) is now readable via HTTP. Two new tests added (`GetEvidenceForProposalAsync_ReturnsEvidenceAttachedDuringApproval` + the endpoint itself). `dotnet build -c Release` clean (0/0). **Not verified against a live database** — same 127.0.0.1:5432 connection-refused blocker as DEBT-017/026; do not mark fully verified until a real Postgres run passes. | @claude | DEBT-017 (2026-08-08), `src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/README.md` | | DEBT-026 | `Features/ApprovalWorkflow` has no wired Draft→Proposed transition | High (3) | Low (1) | Completed (DB verification pending) | Added `ProposeForReviewHandler` + `POST /approvals/{id}/propose`, wired into `Program.cs` DI. Calls the pre-existing `ApprovalWorkflowPolicy.CanProposeForReview` (creator-only) and `ValidateProposalState` (Draft→Proposed), then updates status and emits a `PROPOSED` event — same pattern as `ApproveApprovalHandler`/`ActivateModelHandler`. A proposal created via `POST /approvals` can now reach `Approved`/`Active` through the HTTP API end-to-end. Two new tests added (`ProposeForReview_ByCreatingMaker_TransitionsDraftToProposed`, `ProposeForReview_ByDifferentUserThanCreator_ThrowsUnauthorized`). `dotnet build -c Release` clean (0/0). **Not verified against a live database** — same 127.0.0.1:5432 connection-refused blocker as DEBT-017/025; `dotnet test --filter FullyQualifiedName~ApprovalWorkflowTests -c Release` run 2026-08-08, all 17 matched tests fail with connection-refused (includes this file's tests plus an unrelated top-level `ApprovalWorkflowTests.cs` the substring filter also matches). Do not mark fully verified until a real Postgres run passes. | @claude | DEBT-017 (2026-08-08), `src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/README.md` | +| DEBT-027 | `PollTradeStatusHandler`/`ConfirmSettlementHandler` registered in DI but never invoked by anything | High (3) | Low (1) | Completed (DB verification pending) | Discovered while looking for BE/scheduler priority work (2026-08-09) — same class of gap as DEBT-026 (a fully-implemented handler with no caller). `TradeEndpoints.cs` only has `POST /trades` (→`SubmitTradeHandler`) and `GET /trades`; nothing ever called `PollTradeStatusHandler` or `ConfirmSettlementHandler`, and no Hangfire job did either, so a trade could reach `Submitted` and never progress — KIS fills and settlement confirmations were never picked up. Added `src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs`: a Hangfire recurring job (`trade-status-polling`, every 2 minutes, `q-customer-sla` queue per CLAUDE.md's queue-isolation guidance since this affects real trade completion, not research) that queries `Submitted`/`Accepted`/`PartiallyFilled` trades and calls `PollTradeStatusHandler`, then queries `FullyFilled` trades and calls `ConfirmSettlementHandler`. Registered in `Program.cs` alongside the other recurring jobs. `dotnet build -c Release` clean (0/0). **No dedicated test added** (the job is thin orchestration over the already-implemented, already-covered-elsewhere handlers, and writing a fake `IKisTradeExecutionService`/`ITradeSql` test double would be a new testing pattern not used anywhere else in this codebase — flagged rather than done rashly) **and not run against a live database or KIS** — same connection blocker as the rest of this session's work. | @claude | Session 2026-08-09 (BE/scheduler priority pass) | --- diff --git a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv index d3b819e4..a16d31d2 100644 --- a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv +++ b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv @@ -36,6 +36,6 @@ AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm cha AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice." AEG-VS-10-01,S4,VS-10,매도 결정 엔진 구현 (GenerateSellDecision),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-10-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/SellDecision/ (SellDecisionEndpoints.cs, SellDecisionHandler.cs, SellDecisionSql.cs, SellPriorityRanker.cs); frontend/src/features/sell-decision/; tests/KArtSell.Integration.Tests/SellDecision/SellDecisionTests.cs; commit b1e38ac (Phase 3 J, PR #28, merged to main)",BE Lead/Quant Lead,"✅ Implementation complete (code + BE + FE + tests), matches WBS_MASTER's VS-10='GenerateSellDecision' definition (no ID collision here). Sell priority ranking (HARD_IMPAIRMENT→...→REENTRY_OPTION) with age/liquidity score boosts per VS-10-SLICE_SPEC.md. 32/32 tests PASS run in isolation (2026-08-07); one test (CalculateScore_HardImpairment_ReturnsLowestScore) had a wrong input value that happened to not exercise the >365-day age-boost branch the spec defines — fixed as a test bug, not a product bug (see fix/dapper-underscore-mapping-and-build branch). ⚠️ NOT validated: this row was previously (incorrectly) marked BLOCKED with reasoning 'Model must pass PBO/DSR validation' — that Gate-3/production-readiness validation genuinely still requires real Phase 1 shadow-run data and has not happened. Distinguish 'code implemented and unit/integration-tested' (done) from 'PBO/DSR-validated against real market data' (not done, blocked on Phase 1)." AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice." -AEG-VS-28-01,S2,VS-28,"거래 실행 시스템 구현 (Trade Execution, KIS Integration)",COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-28-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/TradeExecution/ (TradeEndpoints.cs, TradeHandlers.cs, TradeSql.cs, Trade.cs, KisTradeExecutionService.cs); db/migrations/0039_trades.sql; tests/KArtSell.Integration.Tests/TradeExecution/TradeExecutionTests.cs; commit b1e38ac (Phase 3 K, PR #28, merged to main)",BE Lead/Trading Ops,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Trade state machine (Pending→Submitted→Accepted→PartiallyFilled/FullyFilled→Confirmed→Reconciled), KIS order submission/poll/settlement. 13/13 tests PASS run in isolation (2026-08-07), but only after two real bugs were fixed on fix/dapper-underscore-mapping-and-build: (1) UpdateTradeStatusAsync only ever persisted status/kis_response/error_message and silently dropped kis_order_id, executed_quantity, unit_price, commission, net_proceeds and both timestamps on every single call since the slice merged — trade fills and settlements were not actually being recorded; (2) the same Dapper snake_case-mapping race condition described in AEG-VS-27-01's notes. ⚠️ No frontend UI yet (no frontend/src/features/trade-execution/) — an attempt was started 2026-08-08 but the background agent building it failed (hit the session's monthly spend limit) before producing any committed code; not resumed this session. Renumbered from VS-12 to VS-28 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-12 in WBS_MASTER.csv ('RankBuyCandidates') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 2026-08-08 (BE priority pass): DEBT-018 (outbox write not co-transactional with the trade status update) fixed — see TECH_DEBT_REGISTER.md; `dotnet build -c Release` clean, DB-backed tests still unverified (no reachable Postgres this session)." +AEG-VS-28-01,S2,VS-28,"거래 실행 시스템 구현 (Trade Execution, KIS Integration)",COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-28-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/TradeExecution/ (TradeEndpoints.cs, TradeHandlers.cs, TradeSql.cs, Trade.cs, KisTradeExecutionService.cs); db/migrations/0039_trades.sql; tests/KArtSell.Integration.Tests/TradeExecution/TradeExecutionTests.cs; commit b1e38ac (Phase 3 K, PR #28, merged to main)",BE Lead/Trading Ops,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Trade state machine (Pending→Submitted→Accepted→PartiallyFilled/FullyFilled→Confirmed→Reconciled), KIS order submission/poll/settlement. 13/13 tests PASS run in isolation (2026-08-07), but only after two real bugs were fixed on fix/dapper-underscore-mapping-and-build: (1) UpdateTradeStatusAsync only ever persisted status/kis_response/error_message and silently dropped kis_order_id, executed_quantity, unit_price, commission, net_proceeds and both timestamps on every single call since the slice merged — trade fills and settlements were not actually being recorded; (2) the same Dapper snake_case-mapping race condition described in AEG-VS-27-01's notes. ⚠️ Frontend UI built 2026-08-09 (frontend/src/features/trade-execution/, route /ops/trade-execution, pnpm typecheck/build clean, 13 new tests passing) after an earlier attempt failed on the session spend limit and was resumed — on isolated worktree branch worktree-agent-aae90f132a2daf359 (HEAD predates the VS-12→VS-28 renumbering, so that worktree's own tracker row is still AEG-VS-12-01), not yet merged into this branch. Found DEBT-025 there too: TradeEndpoints.cs is AllowAnonymous() with no Roles()/Policies() at all (unlike SellDecisionEndpoints.cs); collides with two other independently-numbered DEBT-025 entries on other unmerged branches — renumber on merge. Renumbered from VS-12 to VS-28 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-12 in WBS_MASTER.csv ('RankBuyCandidates') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 2026-08-08 (BE priority pass): DEBT-018 (outbox write not co-transactional with the trade status update) fixed — see TECH_DEBT_REGISTER.md; `dotnet build -c Release` clean, DB-backed tests still unverified (no reachable Postgres this session). 2026-08-09: DEBT-027 fixed — PollTradeStatusHandler/ConfirmSettlementHandler were registered in DI but never invoked by anything (no endpoint, no job); added src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs as a Hangfire recurring job so submitted trades actually progress to Confirmed. `dotnet build -c Release` clean; no dedicated test added (see TECH_DEBT_REGISTER.md for why) and not run against a live database/KIS." AEG-VS-29-01,S2,VS-29,포트폴리오 대사 구현 (Portfolio Reconciliation),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-29-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/ (Endpoints.cs, ReconcileTradeHandler.cs, ReconciliationEngine.cs, ReconciliationSql.cs, MismatchDetector.cs, CostBasisCalculator.cs); tests/KArtSell.Integration.Tests/PortfolioReconciliation/ReconciliationEngineTests.cs; tests/KArtSell.ModelOperations.UnitTests/EvaluationReconciliationPlannerTests.cs; commit b1e38ac (Phase 3 L, PR #28, merged to main); commit 4059828 (fix: missing model_operations.models table breaking every fresh DB, PR #29, merged to main same day)",BE Lead,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Sell Decision → Trade → Holdings reconciliation, cost-basis calculation, mismatch detection. 18/18 tests PASS run in isolation (2026-08-07). Note this slice's own merge (PR #28) shipped with a missing model_operations.models table that broke every fresh-database migration; that was caught and fixed same day in PR #29 — a reminder that this branch's fresh-install DbUp path had not actually been rehearsed before merge. ⚠️ No frontend UI yet — an attempt was started 2026-08-08 but the background agent building it failed (hit the session's monthly spend limit) before producing any committed code; not resumed this session. Renumbered from VS-14 to VS-29 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-14 in WBS_MASTER.csv ('GenerateDailyRecommendations') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 2026-08-08 (BE priority pass): DEBT-018 (outbox writes for TradeReconciled/ReconciliationMismatchAlert not co-transactional with the holding/log write) fixed — see TECH_DEBT_REGISTER.md; `dotnet build -c Release` clean, DB-backed tests still unverified (no reachable Postgres this session)." PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; evidence/AEG-X-004/production-readonly-preflight-20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Read-only preflight: active DbUp journal public.kartsell_schema_versions contains 0032 and check_status includes Queued. Capabilities remain order/KIS/client publication OFF. Server-side dataset_manifest, model_version_registry, evidence_snapshot, and release_evidence_bundle contain no approved/frozen rows; no RunId/JobId/enqueue created. Blocked pending approved server-side VersionSet. Re-confirmed 2026-08-07: still no RunId/JobId exists anywhere in this workspace or its evidence trail; nothing changed on this row this session. Any future document that claims this row is RUNNING must cite a real RunId/JobId — do not restate the earlier (already-corrected) false claim." diff --git a/src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs b/src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs new file mode 100644 index 00000000..64fe33ae --- /dev/null +++ b/src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs @@ -0,0 +1,112 @@ +using Hangfire; +using KArtSell.Modules.ModelOperations.TradeExecution; +using Microsoft.Extensions.Logging; + +namespace KArtSell.Host.Jobs; + +/// +/// Polls KIS for status on trades that have been submitted but haven't reached a terminal state, +/// and confirms settlement for trades that have fully filled. +/// +/// DEBT-027: before this job existed, PollTradeStatusHandler and ConfirmSettlementHandler were +/// registered in DI and fully implemented, but nothing in the running application ever invoked +/// them — no endpoint, no scheduled job. A trade could reach Submitted via POST /trades and never +/// progress any further; the same class of gap DEBT-026 found and fixed for the approval +/// workflow's Draft->Proposed transition. +/// +public sealed class TradeStatusPollingJob( + ITradeSql sql, + PollTradeStatusHandler pollHandler, + ConfirmSettlementHandler settlementHandler, + ILogger logger) +{ + private static readonly TradeStatus[] PollableStatuses = + [TradeStatus.Submitted, TradeStatus.Accepted, TradeStatus.PartiallyFilled]; + + private static readonly Action LogPolled = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(LogPolled)), + "Trade status polling processed {TradeCount} non-terminal trades."); + + private static readonly Action LogPollError = + LoggerMessage.Define( + LogLevel.Error, + new EventId(2, nameof(LogPollError)), + "Failed to poll trade status for {TradeId}"); + + private static readonly Action LogSettled = + LoggerMessage.Define( + LogLevel.Information, + new EventId(3, nameof(LogSettled)), + "Trade settlement confirmation processed {TradeCount} fully-filled trades."); + + private static readonly Action LogSettleError = + LoggerMessage.Define( + LogLevel.Error, + new EventId(4, nameof(LogSettleError)), + "Failed to confirm settlement for {TradeId}"); + + [Queue("q-customer-sla")] + [DisableConcurrentExecution(timeoutInSeconds: 120)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task ExecuteAsync(CancellationToken ct = default) + { + var polledCount = 0; + foreach (var status in PollableStatuses) + { + var trades = await sql.GetTradesByStatusAsync(status, Guid.NewGuid(), ct); + foreach (var trade in trades) + { + if (string.IsNullOrEmpty(trade.KisOrderId)) + { + continue; + } + + try + { + await pollHandler.HandleAsync(new PollTradeStatusCommand + { + TradeId = trade.Id, + KisOrderId = trade.KisOrderId, + CorrelationId = trade.CorrelationId + }, ct); + polledCount++; + } + catch (Exception ex) + { + LogPollError(logger, trade.Id, ex); + } + } + } + + LogPolled(logger, polledCount, null); + + var filledTrades = await sql.GetTradesByStatusAsync(TradeStatus.FullyFilled, Guid.NewGuid(), ct); + var settledCount = 0; + foreach (var trade in filledTrades) + { + if (string.IsNullOrEmpty(trade.KisOrderId)) + { + continue; + } + + try + { + await settlementHandler.HandleAsync(new ConfirmSettlementCommand + { + TradeId = trade.Id, + KisOrderId = trade.KisOrderId, + CorrelationId = trade.CorrelationId + }, ct); + settledCount++; + } + catch (Exception ex) + { + LogSettleError(logger, trade.Id, ex); + } + } + + LogSettled(logger, settledCount, null); + } +} diff --git a/src/KArtSell.Host/Program.cs b/src/KArtSell.Host/Program.cs index e0b9ad56..843fd3fe 100644 --- a/src/KArtSell.Host/Program.cs +++ b/src/KArtSell.Host/Program.cs @@ -200,6 +200,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Portfolio Reconciliation (VS-14) builder.Services.AddScoped(); @@ -397,6 +398,8 @@ try { RecurringJob.AddOrUpdate("opendart-daily-batch", jo try { RecurringJob.AddOrUpdate("daily-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ daily-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ daily-recommendation error"); } try { RecurringJob.AddOrUpdate("weekly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * 6", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ weekly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ weekly-recommendation error"); } try { RecurringJob.AddOrUpdate("monthly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 1 * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ monthly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ monthly-recommendation error"); } +// DEBT-027: without this, PollTradeStatusHandler/ConfirmSettlementHandler were reachable via DI but never invoked by anything. +try { RecurringJob.AddOrUpdate("trade-status-polling", job => job.ExecuteAsync(CancellationToken.None), "*/2 * * * *", new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); logger.LogInformation("✅ trade-status-polling registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ trade-status-polling error"); } logger.LogInformation("🎯 Hangfire background jobs initialization complete");