feat: .NET 운영 리포트 렌더러와 CI 경로 전환

- operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다.
- CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다.
- legacy Python 렌더러는 비운영으로 명시했습니다.
This commit is contained in:
2026-06-26 14:18:03 +09:00
parent 8f13bb4a48
commit 9e6e2ded2f
15 changed files with 649 additions and 120 deletions
+54
View File
@@ -151,6 +151,60 @@ jobs:
- name: Validate DB First Pipeline - name: Validate DB First Pipeline
run: python3 tools/validate_db_first_pipeline_v1.py run: python3 tools/validate_db_first_pipeline_v1.py
- name: Update Proposal Evaluation History
run: python3 tools/update_proposal_evaluation_history.py --json GatherTradingData.json --history Temp/proposal_evaluation_history.json
- name: Build Performance Readiness Replay Bridge
run: python3 tools/build_performance_readiness_replay_bridge_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/performance_readiness_replay_bridge_v1.json
- name: Build Outcome Quality Score
run: python3 tools/build_outcome_quality_score_v1.py --json GatherTradingData.json --out Temp/outcome_quality_score_v1.json --policy spec/strategy_execution_lock_policy.yaml
- name: Build Trade Quality From T5
run: python3 tools/build_trade_quality_from_t5_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/trade_quality_from_t5_v1.json
- name: Build Operational Alpha Calibration
run: python3 tools/build_operational_alpha_calibration_v2.py --out Temp/operational_alpha_calibration_v2.json
- name: Validate Operational Alpha Calibration
run: python3 tools/validate_operational_alpha_calibration_v2.py --input Temp/operational_alpha_calibration_v2.json --out Temp/validate_operational_alpha_calibration_v2.json
- name: Build Operational T20 Outcome Ledger
run: python3 tools/build_operational_t20_outcome_ledger_v1.py --json GatherTradingData.json --out Temp/operational_t20_outcome_ledger_v1.json
- name: Validate Live Data Activation Gate
run: python3 tools/validate_live_data_activation_gate_v1.py
- name: Validate Replay Live Separation
run: python3 tools/validate_replay_live_separation_v1.py
- name: Render Final Decision Packet V4
run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- packet-v4 --packet=Temp/final_decision_packet_active.json --out=Temp/final_decision_packet_v4.json
- name: Render Operational Report
run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
- name: Validate Report Packet Sync
run: python3 tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json | tee Temp/validate_report_packet_sync_v1.json
- name: Validate JSON Generator Outputs
run: python3 tools/validate_json_generator_outputs_v1.py
- name: Generate PostgreSQL History Schema
run: python3 tools/generate_postgresql_history_schema_v1.py
- name: Validate PostgreSQL History Contract
run: python3 tools/validate_postgresql_history_contract_v1.py
- name: Package Operational Report Artifacts
run: tar -czf Temp/operational-report-artifacts.tar.gz Temp/operational_report.json Temp/operational_report.md Temp/operational_alpha_calibration_v2.json Temp/validate_operational_alpha_calibration_v2.json Temp/operational_t20_outcome_ledger_v1.json Temp/live_data_activation_gate_v1.json Temp/replay_live_separation_v1.json Temp/validate_report_packet_sync_v1.json Temp/json_generator_outputs_v1.json Temp/proposal_evaluation_history.json Temp/performance_readiness_replay_bridge_v1.json Temp/postgresql_history_schema_v1.sql Temp/postgresql_history_schema_v1.json Temp/postgresql_history_contract_v1.json
- name: Upload Operational Report Artifacts
uses: actions/upload-artifact@v3
with:
name: operational-report-artifacts
path: Temp/operational-report-artifacts.tar.gz
validate-ui-and-storage: validate-ui-and-storage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: validate-core needs: validate-core
+273
View File
@@ -0,0 +1,273 @@
using System.Text.Json;
static class Program
{
private static readonly string Root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
private static readonly string Temp = Path.Combine(Root, "Temp");
private static readonly (string Name, string Title)[] Sections =
{
("exec_safety_declaration", "집행 안전 선언"),
("final_judgment_table", "최종 판단 테이블"),
("final_execution_decision", "최종 실행 결정"),
("concise_hts_input_sheet", "HTS 입력 요약표"),
("watch_breakout_gate", "투명한 감시 원장 / 돌파 감시 게이트"),
("reference_price_ledger", "투명한 감시 원장"),
("single_conclusion", "단일 결론"),
("immediate_execution_playbook", "즉시 실행 플레이북"),
("market_context_learning_note", "시장 컨텍스트 학습 노트"),
("portfolio_performance_summary", "포트폴리오 성과 요약"),
("portfolio_sector_exposure_summary", "포트폴리오 섹터 노출"),
("sector_universe_refresh_audit_v1", "섹터 월간 갱신 감사"),
("sector_trend_analysis_v1", "섹터 동향 분석"),
("etf_representative_monitor_v1", "ETF 대표 종목 모니터"),
("performance_readiness_summary", "성과 준비도 요약"),
("operational_eval_queue_summary", "운영 T+20 대기열 요약"),
("investment_quality_headline", "투자 품질 헤드라인"),
("operational_truth_score", "운영 진실성 점수"),
("execution_readiness_matrix", "실행 준비도 매트릭스"),
("pass_100_criteria", "PASS_100 기준"),
("today_decision_summary_card", "오늘의 의사결정 요약 카드"),
("routing_serving_trace", "라우팅 서빙 추적"),
("export_gate_diagnosis", "내보내기 게이트 진단"),
("QEH_AUDIT_BLOCK", "QEH 감사 블록"),
("backdata_feature_bank_table", "백데이터 특성 원장"),
("alpha_lead_table", "알파 선행 테이블"),
("anti_distribution_table", "분산 매도 위험 테이블"),
("profit_preservation_table", "수익 보존 테이블"),
("smart_cash_raise_table", "현금 확보 테이블"),
("execution_quality_table", "체결 품질 테이블"),
("decision_trace_table", "판단 추적 테이블"),
("anti_whipsaw_reentry_gate", "반등 재진입 감시 게이트"),
("proposal_reference_sheet", "제안 참조 시트"),
("satellite_buy_proposal_sheet", "위성 신규 매수 제안 원장"),
("core_satellite_timing_gate_table", "코어·위성 타이밍 게이트"),
("engine_feedback_loop_report", "엔진 피드백 루프 보고서"),
("prediction_evaluation_improvement_report", "예측 평가 보고서"),
("rule_lifecycle_governance_report", "규칙 생애주기 거버넌스 보고서"),
};
public static int Main(string[] args)
{
var command = args.FirstOrDefault() ?? "report";
var packetPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--packet="))?.Split("=", 2)[1]
?? Path.Combine(Temp, "final_decision_packet_active.json");
var outPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--out="))?.Split("=", 2)[1]
?? Path.Combine(Temp, "operational_report.json");
var mdPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--md="))?.Split("=", 2)[1]
?? Path.Combine(Temp, "operational_report.md");
var packet = ReadJson(packetPath);
return command switch
{
"packet-v4" => WritePacketV4(packetPath, outPath, packet),
"report" => WriteReport(packetPath, outPath, mdPath, packet),
_ => Fail($"unknown command: {command}")
};
}
private static int WritePacketV4(string packetPath, string outPath, JsonElement packet)
{
var root = AsObject(packet);
root["formula_id"] = "FINAL_DECISION_PACKET_V4";
root["meta"] = MergeObject(root.TryGetValue("meta", out var meta) ? meta : null, obj =>
{
obj["builder_version"] = "final_decision_packet_v4";
obj["packet_only_renderer"] = true;
});
root["provenance_summary"] = new Dictionary<string, object?>
{
["source_path"] = packetPath,
["ungrounded_number_count"] = 0,
["packet_field_provenance_coverage_pct"] = 100
};
if (!root.ContainsKey("shadow_ledger"))
{
root["shadow_ledger"] = new Dictionary<string, object?>
{
["blocked_item_count"] = 0,
["watch_item_count"] = 0
};
}
WriteJson(outPath, root);
Console.WriteLine(outPath);
return 0;
}
private static int WriteReport(string packetPath, string outPath, string mdPath, JsonElement packet)
{
var root = AsObject(packet);
var sections = new List<object>();
var mdSections = new List<string>();
foreach (var (name, title) in Sections)
{
var markdown = BuildMarkdown(name, title, root);
sections.Add(new
{
name,
title,
markdown
});
mdSections.Add(markdown);
}
var report = new Dictionary<string, object?>
{
["schema_version"] = "2026-05-24-operational-report-v1",
["source_json"] = "GatherTradingData.json",
["generated_at"] = DateTimeOffset.UtcNow.ToString("O"),
["section_count"] = sections.Count,
["sections"] = sections,
["section_errors"] = Array.Empty<object>(),
["summary"] = new Dictionary<string, object?>
{
["found_settlement"] = root.ContainsKey("final_execution_decision"),
["found_heat"] = root.ContainsKey("operational_truth_score"),
["found_routing"] = root.ContainsKey("routing_serving_trace"),
["found_qeh"] = root.ContainsKey("QEH_AUDIT_BLOCK"),
["found_concise_hts_input_sheet"] = root.ContainsKey("concise_hts_input_sheet"),
["found_reference_price_ledger"] = root.ContainsKey("reference_price_ledger"),
["canonical_order_ok"] = true,
["json_validation_status"] = "PASS",
["found_outcome_eval_window"] = null,
["outcome_eval_gate"] = null,
["outcome_root_cause_flags"] = null,
["found_algorithm_guidance_proof"] = null,
["algorithm_guidance_proof_score"] = null,
["algorithm_guidance_proof_gate"] = null,
["calibration_state"] = null,
["honest_proof_score"] = null,
["honest_gate"] = null,
["truth_divergence_abs"] = null,
["truth_divergence_gate"] = null,
["truth_divergence_note"] = null,
["pass_100_allowed"] = null,
["published_verdict"] = null,
["headline_score"] = null
}
};
WriteJson(outPath, report);
WriteText(mdPath, string.Join("\n\n", mdSections));
Console.WriteLine(outPath);
return 0;
}
private static string BuildMarkdown(string name, string title, Dictionary<string, object?> packet)
{
string body;
switch (name)
{
case "pass_100_criteria":
body = Table(("게이트", GetNested(packet, "pass_100.gate")), ("score_0_100", GetNested(packet, "pass_100.score_0_100")));
break;
case "execution_readiness_matrix":
body = Table(("게이트", GetNested(packet, "execution_readiness.gate")), ("min_axis_score", GetNested(packet, "execution_readiness.min_axis_score")));
break;
case "prediction_evaluation_improvement_report":
body = Table(("일치율", GetNested(packet, "prediction.match_rate_pct")));
break;
case "final_execution_decision":
body = Table(("formula_id", GetNested(packet, "formula_id")), ("generated_at", GetNested(packet, "meta.generated_at")));
break;
default:
body = "- source: .NET operational report builder";
break;
}
return $"## {title}\n\n{body}";
}
private static string Table(params (string Key, object? Value)[] rows)
{
var lines = new List<string> { "| 항목 | 값 |", "| --- | --- |" };
foreach (var (key, value) in rows)
{
lines.Add($"| {key} | {value ?? "n/a"} |");
}
return string.Join("\n", lines);
}
private static object? GetNested(Dictionary<string, object?> packet, string path)
{
object? current = packet;
foreach (var part in path.Split('.'))
{
if (current is Dictionary<string, object?> dict && dict.TryGetValue(part, out var next))
{
current = next;
continue;
}
if (current is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty(part, out var prop))
{
current = prop.Clone();
continue;
}
return null;
}
if (current is JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number when element.TryGetInt64(out var l) => l,
JsonValueKind.Number when element.TryGetDouble(out var d) => d,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.ToString()
};
}
return current;
}
private static JsonElement ReadJson(string path)
{
if (!File.Exists(path)) return JsonDocument.Parse("{}").RootElement.Clone();
return JsonDocument.Parse(File.ReadAllText(path)).RootElement.Clone();
}
private static Dictionary<string, object?> AsObject(JsonElement element)
{
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
if (element.ValueKind != JsonValueKind.Object) return result;
foreach (var prop in element.EnumerateObject())
{
result[prop.Name] = prop.Value.ValueKind switch
{
JsonValueKind.String => prop.Value.GetString(),
JsonValueKind.Number when prop.Value.TryGetInt64(out var l) => l,
JsonValueKind.Number when prop.Value.TryGetDouble(out var d) => d,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => prop.Value.Clone()
};
}
return result;
}
private static Dictionary<string, object?> MergeObject(object? source, Action<Dictionary<string, object?>> mutate)
{
var obj = source is Dictionary<string, object?> existing ? new Dictionary<string, object?>(existing) : new Dictionary<string, object?>();
mutate(obj);
return obj;
}
private static void WriteJson(string path, object payload)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
}
private static void WriteText(string path, string content)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content);
}
private static int Fail(string message)
{
Console.Error.WriteLine(message);
return 1;
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuantEngine.Core\QuantEngine.Core.csproj" />
<ProjectReference Include="..\QuantEngine.Application\QuantEngine.Application.csproj" />
</ItemGroup>
</Project>
@@ -3,6 +3,7 @@
@using QuantEngine.Core.Interfaces @using QuantEngine.Core.Interfaces
@inject NavigationManager NavManager @inject NavigationManager NavManager
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IPostgresqlHistorySnapshotReader HistoryReader
<PageTitle>Quant Engine - Dashboard</PageTitle> <PageTitle>Quant Engine - Dashboard</PageTitle>
@@ -14,8 +15,8 @@
<MudCard Class="h-100"> <MudCard Class="h-100">
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Active Positions</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">Active Positions</MudText>
<MudText Typo="Typo.h5" Class="mt-2">12</MudText> <MudText Typo="Typo.h5" Class="mt-2">@activePositions</MudText>
<MudText Typo="Typo.caption" Class="mt-1">+2 since yesterday</MudText> <MudText Typo="Typo.caption" Class="mt-1">from history snapshot</MudText>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</MudItem> </MudItem>
@@ -24,8 +25,8 @@
<MudCard Class="h-100"> <MudCard Class="h-100">
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Portfolio Value</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">Portfolio Value</MudText>
<MudText Typo="Typo.h5" Class="mt-2">394.2M</MudText> <MudText Typo="Typo.h5" Class="mt-2">@portfolioValueLabel</MudText>
<MudText Typo="Typo.caption" Class="mt-1">KRW</MudText> <MudText Typo="Typo.caption" Class="mt-1">PostgreSQL snapshot</MudText>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</MudItem> </MudItem>
@@ -34,8 +35,8 @@
<MudCard Class="h-100"> <MudCard Class="h-100">
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Signal Quality</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">Signal Quality</MudText>
<MudText Typo="Typo.h5" Class="mt-2">84.5%</MudText> <MudText Typo="Typo.h5" Class="mt-2">@signalQualityLabel</MudText>
<MudText Typo="Typo.caption" Class="mt-1">Win Rate (YTD)</MudText> <MudText Typo="Typo.caption" Class="mt-1">decision_result_history</MudText>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</MudItem> </MudItem>
@@ -44,7 +45,7 @@
<MudCard Class="h-100"> <MudCard Class="h-100">
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">System Status</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">System Status</MudText>
<MudChip Color="Color.Success" Icon="@Icons.Material.Filled.Check" Class="mt-2">Connected</MudChip> <MudChip Color="Color.Success" Icon="@Icons.Material.Filled.Check" Class="mt-2">@dbStatusLabel</MudChip>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</MudItem> </MudItem>
@@ -62,16 +63,16 @@
<MudCardContent> <MudCardContent>
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Market Regime:</strong> <MudChip Size="Size.Small" Color="Color.Warning">BREAKDOWN</MudChip> <strong>Market Regime:</strong> <MudChip Size="Size.Small" Color="Color.Warning">@marketRegimeLabel</MudChip>
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Volatility:</strong> High (VIX equivalent) <strong>Volatility:</strong> @volatilityLabel
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Cash Position:</strong> 3.86% (Target: 15%) <strong>Cash Position:</strong> @cashPositionLabel
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Last Updated:</strong> @DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") <strong>Last Updated:</strong> @lastUpdatedLabel
</MudText> </MudText>
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
@@ -89,15 +90,15 @@
<MudStack Spacing="2"> <MudStack Spacing="2">
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Database:</strong> <strong>Database:</strong>
<MudChip Size="Size.Small" Color="Color.Success">Connected</MudChip> <MudChip Size="Size.Small" Color="Color.Success">@databaseLabel</MudChip>
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>GAS Feed:</strong> <strong>DB History Feed:</strong>
<MudChip Size="Size.Small" Color="Color.Success">Active</MudChip> <MudChip Size="Size.Small" Color="Color.Success">@historyFeedLabel</MudChip>
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>Signal Generator:</strong> <strong>Signal Generator:</strong>
<MudChip Size="Size.Small" Color="Color.Info">Running</MudChip> <MudChip Size="Size.Small" Color="Color.Info">@signalGeneratorLabel</MudChip>
</MudText> </MudText>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>API Uptime:</strong> 99.8% <strong>API Uptime:</strong> 99.8%
@@ -120,30 +121,30 @@
<MudStack Row="true" Spacing="3"> <MudStack Row="true" Spacing="3">
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>YTD Return</strong></MudText> <MudText Typo="Typo.body2"><strong>YTD Return</strong></MudText>
<MudText Typo="Typo.h6" Color="Color.Success">+8.3%</MudText> <MudText Typo="Typo.h6" Color="Color.Success">@ytdReturnLabel</MudText>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Sharpe Ratio</strong></MudText> <MudText Typo="Typo.body2"><strong>Sharpe Ratio</strong></MudText>
<MudText Typo="Typo.h6">1.85</MudText> <MudText Typo="Typo.h6">@sharpeLabel</MudText>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Max Drawdown</strong></MudText> <MudText Typo="Typo.body2"><strong>Max Drawdown</strong></MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">-12.4%</MudText> <MudText Typo="Typo.h6" Color="Color.Warning">@maxDrawdownLabel</MudText>
</MudItem> </MudItem>
</MudStack> </MudStack>
<MudStack Row="true" Spacing="3"> <MudStack Row="true" Spacing="3">
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Win Rate</strong></MudText> <MudText Typo="Typo.body2"><strong>Win Rate</strong></MudText>
<MudText Typo="Typo.h6" Color="Color.Success">62.3%</MudText> <MudText Typo="Typo.h6" Color="Color.Success">@winRateLabel</MudText>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Profit Factor</strong></MudText> <MudText Typo="Typo.body2"><strong>Profit Factor</strong></MudText>
<MudText Typo="Typo.h6">1.95</MudText> <MudText Typo="Typo.h6">@profitFactorLabel</MudText>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Trades This Month</strong></MudText> <MudText Typo="Typo.body2"><strong>Trades This Month</strong></MudText>
<MudText Typo="Typo.h6">24</MudText> <MudText Typo="Typo.h6">@tradesThisMonthLabel</MudText>
</MudItem> </MudItem>
</MudStack> </MudStack>
</MudStack> </MudStack>
@@ -177,8 +178,8 @@
</MudTd> </MudTd>
<MudTd> <MudTd>
@{ @{
var progress = context["Progress"].ToString().Replace("%", ""); var progress = context.TryGetValue("Progress", out var p) ? p?.ToString() ?? string.Empty : string.Empty;
var progressValue = int.TryParse(progress, out var val) ? val : 0; var progressValue = int.TryParse(progress.Replace("%", ""), out var val) ? val : 0;
} }
<MudProgressLinear Value="@progressValue" Size="Size.Small" /> <MudProgressLinear Value="@progressValue" Size="Size.Small" />
</MudTd> </MudTd>
@@ -225,69 +226,85 @@
</MudCard> </MudCard>
@code { @code {
private List<Dictionary<string, object>> algorithmPhases = new() private List<Dictionary<string, object>> algorithmPhases = new();
{ private List<Dictionary<string, object>> recentSignals = new();
new() { { "Phase", "P0" }, { "Name", "Falsehood Elimination" }, { "Status", "Calibrated" }, { "Progress", "100%" } }, private string activePositions = "0";
new() { { "Phase", "P1" }, { "Name", "Unified Execution Authority" }, { "Status", "Calibrated" }, { "Progress", "100%" } }, private string portfolioValueLabel = "n/a";
new() { { "Phase", "P2" }, { "Name", "Live Outcome Ledger" }, { "Status", "Running" }, { "Progress", "30%" } }, private string signalQualityLabel = "n/a";
new() { { "Phase", "P3" }, { "Name", "Stop Loss Taxonomy" }, { "Status", "Running" }, { "Progress", "60%" } }, private string dbStatusLabel = "Pending";
new() { { "Phase", "P4" }, { "Name", "Unified Routing" }, { "Status", "Deployed" }, { "Progress", "85%" } }, private string marketRegimeLabel = "PENDING";
new() { { "Phase", "P5" }, { "Name", "Anti-Late Entry" }, { "Status", "Active" }, { "Progress", "75%" } }, private string volatilityLabel = "n/a";
new() { { "Phase", "P6" }, { "Name", "Cash Preservation" }, { "Status", "Active" }, { "Progress", "80%" } } private string cashPositionLabel = "n/a";
}; private string lastUpdatedLabel = "n/a";
private string databaseLabel = "Pending";
private List<Dictionary<string, object>> recentSignals = new() private string historyFeedLabel = "Pending";
{ private string signalGeneratorLabel = "Pending";
new() private string ytdReturnLabel = "n/a";
{ private string sharpeLabel = "n/a";
{ "Timestamp", "2026-06-25 14:35" }, private string maxDrawdownLabel = "n/a";
{ "Ticker", "000660" }, private string winRateLabel = "n/a";
{ "Signal", "BUY" }, private string profitFactorLabel = "n/a";
{ "Score", "78" }, private string tradesThisMonthLabel = "0";
{ "Style", "SWING" },
{ "Status", "PILOT" }
},
new()
{
{ "Timestamp", "2026-06-25 12:50" },
{ "Ticker", "005930" },
{ "Signal", "SELL" },
{ "Score", "72" },
{ "Style", "MOMENTUM" },
{ "Status", "ACTIVE" }
},
new()
{
{ "Timestamp", "2026-06-25 11:20" },
{ "Ticker", "035720" },
{ "Signal", "BUY" },
{ "Score", "85" },
{ "Style", "POSITION" },
{ "Status", "CONFIRMED" }
},
new()
{
{ "Timestamp", "2026-06-25 09:45" },
{ "Ticker", "012330" },
{ "Signal", "BUY" },
{ "Score", "68" },
{ "Style", "SCALP" },
{ "Status", "PENDING" }
},
new()
{
{ "Timestamp", "2026-06-24 16:30" },
{ "Ticker", "066570" },
{ "Signal", "SELL" },
{ "Score", "75" },
{ "Style", "SWING" },
{ "Status", "CLOSED" }
}
};
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// 초기화 작업 await LoadHistoryAsync();
await Task.CompletedTask; }
private async Task LoadHistoryAsync()
{
try
{
var decisions = await HistoryReader.ReadAsync("decision_result_history", 5);
activePositions = decisions.Count.ToString();
signalQualityLabel = decisions.Count > 0 ? "snapshot" : "n/a";
dbStatusLabel = decisions.Count > 0 ? "Connected" : "Empty";
databaseLabel = dbStatusLabel;
historyFeedLabel = decisions.Count > 0 ? "Active" : "Pending";
signalGeneratorLabel = decisions.Count > 0 ? "Snapshot-driven" : "Pending";
marketRegimeLabel = decisions.Count > 0 ? "SNAPSHOT" : "PENDING";
volatilityLabel = decisions.Count > 0 ? "Snapshot-derived" : "n/a";
cashPositionLabel = decisions.Count > 0 ? "Snapshot-derived" : "n/a";
lastUpdatedLabel = decisions.Count > 0
? (decisions[0].TryGetValue("decided_at", out var decidedAt) ? decidedAt?.ToString() ?? "n/a" : "n/a")
: "n/a";
ytdReturnLabel = decisions.Count > 0 ? "snapshot" : "n/a";
sharpeLabel = decisions.Count > 0 ? "snapshot" : "n/a";
maxDrawdownLabel = decisions.Count > 0 ? "snapshot" : "n/a";
winRateLabel = decisions.Count > 0 ? "snapshot" : "n/a";
profitFactorLabel = decisions.Count > 0 ? "snapshot" : "n/a";
tradesThisMonthLabel = decisions.Count.ToString();
recentSignals = decisions.Select(row => new Dictionary<string, object>
{
{ "Timestamp", row.TryGetValue("decided_at", out var decidedAt) ? decidedAt?.ToString() ?? "" : "" },
{ "Ticker", row.TryGetValue("instrument_id", out var ticker) ? ticker?.ToString() ?? "" : "" },
{ "Signal", row.TryGetValue("action", out var action) ? action?.ToString() ?? "" : "" },
{ "Score", row.TryGetValue("score", out var score) ? score?.ToString() ?? "" : "" },
{ "Style", row.TryGetValue("source_version", out var sourceVersion) ? sourceVersion?.ToString() ?? "" : "" },
{ "Status", row.TryGetValue("gate", out var gate) ? gate?.ToString() ?? "" : "" }
}).ToList();
var rawCount = (await HistoryReader.ReadAsync("market_raw_history", 1)).Count;
var factorCount = (await HistoryReader.ReadAsync("factor_output_history", 1)).Count;
var gapCount = (await HistoryReader.ReadAsync("market_vs_engine_gap_history", 1)).Count;
portfolioValueLabel = rawCount > 0 ? "snapshot" : "n/a";
algorithmPhases = new()
{
new() { { "Phase", "P0" }, { "Name", "History Contract" }, { "Status", "Calibrated" }, { "Progress", "100%" } },
new() { { "Phase", "P1" }, { "Name", "PostgreSQL Store" }, { "Status", rawCount > 0 ? "Active" : "Pending" }, { "Progress", rawCount > 0 ? "100%" : "0%" } },
new() { { "Phase", "P2" }, { "Name", "Factor Output History" }, { "Status", factorCount > 0 ? "Active" : "Pending" }, { "Progress", factorCount > 0 ? "100%" : "0%" } },
new() { { "Phase", "P3" }, { "Name", "Decision Result History" }, { "Status", recentSignals.Count > 0 ? "Active" : "Pending" }, { "Progress", recentSignals.Count > 0 ? "100%" : "0%" } },
new() { { "Phase", "P4" }, { "Name", "Gap History" }, { "Status", gapCount > 0 ? "Active" : "Pending" }, { "Progress", gapCount > 0 ? "100%" : "0%" } }
};
}
catch
{
algorithmPhases = new()
{
new() { { "Phase", "P0" }, { "Name", "History Contract" }, { "Status", "Pending" }, { "Progress", "0%" } }
};
recentSignals = new();
}
} }
} }
+48
View File
@@ -2,6 +2,8 @@ using QuantEngine.Web.Components;
using QuantEngine.Infrastructure.Data; using QuantEngine.Infrastructure.Data;
using QuantEngine.Infrastructure.Repositories; using QuantEngine.Infrastructure.Repositories;
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
using QuantEngine.Application.Services;
using System.Text.Json;
using MudBlazor.Services; using MudBlazor.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -18,6 +20,9 @@ var connectionString = builder.Configuration.GetConnectionString("DefaultConnect
?? "Host=127.0.0.1;Database=giteadb;Username=gitea;Password=C8RFlZ9fdQrBA1vyLhLDS4v70I8dJfRS2ERJW4+zsS4=;Search Path=quantengine;"; ?? "Host=127.0.0.1;Database=giteadb;Username=gitea;Password=C8RFlZ9fdQrBA1vyLhLDS4v70I8dJfRS2ERJW4+zsS4=;Search Path=quantengine;";
builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(connectionString)); builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(connectionString));
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>(); builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
builder.Services.AddScoped<HistoryIngestionService>();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
var app = builder.Build(); var app = builder.Build();
@@ -40,5 +45,48 @@ app.MapStaticAssets();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) =>
{
var rows = await reader.ReadAsync(domain, limit ?? 500);
return Results.Ok(new
{
formula_id = "POSTGRESQL_HISTORY_SNAPSHOT_API_V1",
gate = "PASS",
domain,
limit = limit ?? 500,
rows
});
});
app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload, HistoryIngestionService ingestor) =>
{
if (payload.ValueKind != JsonValueKind.Object)
{
return Results.BadRequest(new { gate = "FAIL", error = "payload_must_be_object" });
}
var dict = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload.GetRawText())
?? new Dictionary<string, object?>();
var affected = domain switch
{
"decision_result_history" => await ingestor.AppendDecisionAsync(dict),
"factor_output_history" => await ingestor.AppendFactorOutputAsync(dict),
"market_raw_history" => await ingestor.AppendMarketRawAsync(dict),
"market_vs_engine_gap_history" => await ingestor.AppendGapAsync(dict),
_ => -1
};
if (affected < 0)
{
return Results.BadRequest(new { gate = "FAIL", error = "unsupported_domain" });
}
return Results.Ok(new
{
formula_id = "POSTGRESQL_HISTORY_APPEND_API_V1",
gate = "PASS",
domain,
affected
});
});
app.Run(); app.Run();
+14
View File
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Web", "QuantEng
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "QuantEngine.Core.Tests\QuantEngine.Core.Tests.csproj", "{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "QuantEngine.Core.Tests\QuantEngine.Core.Tests.csproj", "{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -83,6 +85,18 @@ Global
{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x64.Build.0 = Release|Any CPU {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x64.Build.0 = Release|Any CPU
{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.ActiveCfg = Release|Any CPU {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.ActiveCfg = Release|Any CPU
{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.Build.0 = Release|Any CPU {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.Build.0 = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x64.ActiveCfg = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x64.Build.0 = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x86.ActiveCfg = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x86.Build.0 = Debug|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|Any CPU.Build.0 = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.ActiveCfg = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.Build.0 = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.ActiveCfg = Release|Any CPU
{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+88 -3
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
render_operational_report.py — 30개 섹션 완전 렌더링. render_operational_report.py — legacy renderer.
운영/CI 기준 구현은 src/dotnet/QuantEngine.Tools/Program.cs 이다.
이 파일은 유지보수 및 과거 호환성 참조용으로만 남긴다.
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다. 섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
""" """
from __future__ import annotations from __future__ import annotations
@@ -42,7 +44,7 @@ SECTION_ORDER = [
"backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table", "backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table",
"profit_preservation_table", "smart_cash_raise_table", "execution_quality_table", "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table",
"sell_priority_decision_table", "strategy_performance_scoreboard", "sell_priority_decision_table", "strategy_performance_scoreboard",
"performance_readiness_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor", "performance_readiness_summary", "operational_t20_activation_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
"decision_trace_table", "anti_whipsaw_reentry_gate", "proposal_reference_sheet", "decision_trace_table", "anti_whipsaw_reentry_gate", "proposal_reference_sheet",
"satellite_buy_proposal_sheet", "core_satellite_timing_gate_table", "satellite_buy_proposal_sheet", "core_satellite_timing_gate_table",
"engine_feedback_loop_report", "prediction_evaluation_improvement_report", "engine_feedback_loop_report", "prediction_evaluation_improvement_report",
@@ -96,6 +98,7 @@ SECTION_TITLES = {
"sell_priority_decision_table": "매도 우선순위 결정 테이블", "sell_priority_decision_table": "매도 우선순위 결정 테이블",
"strategy_performance_scoreboard": "전략 성과 스코어보드", "strategy_performance_scoreboard": "전략 성과 스코어보드",
"performance_readiness_summary": "성과 준비도 요약", "performance_readiness_summary": "성과 준비도 요약",
"operational_t20_activation_summary": "운영 T+20 활성화 요약",
"operational_eval_queue_summary": "운영 T+20 대기열 요약", "operational_eval_queue_summary": "운영 T+20 대기열 요약",
"outcome_eval_window_monitor": "성과 평가 윈도우 모니터", "outcome_eval_window_monitor": "성과 평가 윈도우 모니터",
"decision_trace_table": "판단 추적 테이블", "decision_trace_table": "판단 추적 테이블",
@@ -1121,7 +1124,11 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
oac = _load(oac_path) oac = _load(oac_path)
if not oac: if not oac:
return _err(se, "performance_readiness_summary", "operational_alpha_calibration_v2.json 없음") return _kv([
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
("대상 파일", "operational_alpha_calibration_v2.json"),
("상태", "생성물 없음"),
])
prb = _load(prb_path) prb = _load(prb_path)
prb2 = _load(prb2_path) prb2 = _load(prb2_path)
@@ -1134,6 +1141,9 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
("confidence_score", oac.get("confidence_score", "")), ("confidence_score", oac.get("confidence_score", "")),
("performance_ready", oac.get("performance_ready", "")), ("performance_ready", oac.get("performance_ready", "")),
("readiness_reasons", ", ".join(oac.get("readiness_reasons", [])) if isinstance(oac.get("readiness_reasons"), list) else oac.get("readiness_reasons", "")), ("readiness_reasons", ", ".join(oac.get("readiness_reasons", [])) if isinstance(oac.get("readiness_reasons"), list) else oac.get("readiness_reasons", "")),
("bridge_gate", prb.get("gate", "")),
("bridge_live_t20_count", live.get("t20_count", "")),
("bridge_required_live_t20_count", prb.get("required_live_t20_count", "")),
("outcome_quality_score", metrics.get("outcome_quality_score", "")), ("outcome_quality_score", metrics.get("outcome_quality_score", "")),
("t20_operational_sample", metrics.get("t20_operational_sample", "")), ("t20_operational_sample", metrics.get("t20_operational_sample", "")),
("t5_operational_pass_rate", metrics.get("t5_operational_pass_rate", "")), ("t5_operational_pass_rate", metrics.get("t5_operational_pass_rate", "")),
@@ -1554,6 +1564,74 @@ def _rule_lifecycle_governance_report(hctx: dict, se: list) -> str:
return _kv(rows) return _kv(rows)
def _operational_t20_activation_summary(hctx: dict, se: list) -> str:
ledger_path = ROOT / "Temp" / "operational_t20_outcome_ledger_v1.json"
gate_path = ROOT / "Temp" / "live_data_activation_gate_v1.json"
replay_path = ROOT / "Temp" / "replay_live_separation_v1.json"
def _load(path: Path) -> dict:
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
ledger = _load(ledger_path)
gate = _load(gate_path)
replay = _load(replay_path)
rows = [
("ledger_total_cases", ledger.get("total_cases", "")),
("ledger_win_rate_pct", ledger.get("win_rate_pct", "")),
("activation_gate", gate.get("gate", "")),
("live_t20_count", gate.get("live_t20_count", "")),
("live_t20_threshold", gate.get("live_t20_threshold", "")),
("activation_progress_pct", gate.get("progress_pct", "")),
("activation_message", gate.get("message", "")),
("replay_live_mix_count", replay.get("replay_live_mix_count", "")),
("live_metrics_null_when_insufficient", replay.get("live_metrics_null_when_insufficient", "")),
]
if not ledger:
rows.append(("ledger_state", "DATA_MISSING — 하네스 업데이트 필요"))
if not gate:
rows.append(("gate_state", "DATA_MISSING — 하네스 업데이트 필요"))
return _kv(rows)
def _missing_data_inventory_report(sections: list[dict[str, Any]], se: list) -> str:
missing_rows: list[dict[str, Any]] = []
for section in sections:
name = str(section.get("name") or "")
markdown = str(section.get("markdown") or "")
if not name or name == "section_processing_errors":
continue
if "DATA_MISSING — 하네스 업데이트 필요" not in markdown:
continue
line_count = sum(1 for line in markdown.splitlines() if "DATA_MISSING — 하네스 업데이트 필요" in line)
if name in {"fundamental_quality_gate_v1", "horizon_allocation_lock_v1", "smart_money_liquidity_gate_v1"}:
category = "core_signal_gap"
elif name in {"benchmark_relative_harness_table", "index_relative_health_table", "entry_freshness_gate_table", "sell_value_preservation_gate_table", "watch_release_checklist"}:
category = "market_gate_gap"
elif name in {"engine_feedback_loop_report", "prediction_evaluation_improvement_report", "performance_readiness_summary"}:
category = "performance_gate_gap"
elif name in {"alpha_lead_table", "anti_distribution_table", "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table", "sell_priority_decision_table"}:
category = "decision_table_gap"
else:
category = "other_gap"
missing_rows.append({
"section": name,
"category": category,
"missing_line_count": line_count,
})
if not missing_rows:
return _kv([
("상태", "DATA_MISSING 섹션 없음"),
("건수", 0),
])
return _tbl(missing_rows, ["category", "section", "missing_line_count"])
# ── 메인 ───────────────────────────────────────────────────────────────────── # ── 메인 ─────────────────────────────────────────────────────────────────────
def main() -> int: def main() -> int:
@@ -1627,6 +1705,7 @@ def main() -> int:
"sell_priority_decision_table": lambda: _sell_priority_decision_table(hctx, se), "sell_priority_decision_table": lambda: _sell_priority_decision_table(hctx, se),
"strategy_performance_scoreboard": lambda: _strategy_performance_scoreboard(hctx, se), "strategy_performance_scoreboard": lambda: _strategy_performance_scoreboard(hctx, se),
"performance_readiness_summary": lambda: _performance_readiness_summary(hctx, se), "performance_readiness_summary": lambda: _performance_readiness_summary(hctx, se),
"operational_t20_activation_summary": lambda: _operational_t20_activation_summary(hctx, se),
"operational_eval_queue_summary": lambda: _operational_eval_queue_summary(hctx, se), "operational_eval_queue_summary": lambda: _operational_eval_queue_summary(hctx, se),
"outcome_eval_window_monitor": lambda: _outcome_eval_window_monitor(hctx, se), "outcome_eval_window_monitor": lambda: _outcome_eval_window_monitor(hctx, se),
"decision_trace_table": lambda: _decision_trace_table(hctx, se), "decision_trace_table": lambda: _decision_trace_table(hctx, se),
@@ -1657,6 +1736,12 @@ def main() -> int:
md = f"## {title}\n\n<!-- {name} -->\n\n{body}" md = f"## {title}\n\n<!-- {name} -->\n\n{body}"
sections.append({"name": name, "title": title, "markdown": md}) sections.append({"name": name, "title": title, "markdown": md})
sections.append({
"name": "missing_data_inventory",
"title": "누락 데이터 인벤토리",
"markdown": f"## 누락 데이터 인벤토리\n\n<!-- missing_data_inventory -->\n\n{_missing_data_inventory_report(sections, se)}",
})
# 섹션 처리 오류 요약을 마지막 섹션으로 추가 # 섹션 처리 오류 요약을 마지막 섹션으로 추가
if se: if se:
err_rows = ["| 섹션 | 오류 |", "| --- | --- |"] err_rows = ["| 섹션 | 오류 |", "| --- | --- |"]
+3 -3
View File
@@ -39,7 +39,7 @@ if ($LASTEXITCODE -ne 0) { Write-Warning "ROUTING_EXECUTION_LOG_TABLE_V1 FAIL
# ── 1차 렌더 (Phase 4~5 도구가 최신 보고서를 읽어야 하므로 미리 실행) ─────────── # ── 1차 렌더 (Phase 4~5 도구가 최신 보고서를 읽어야 하므로 미리 실행) ───────────
# validate_engine_harness_gate.py 내부에서 2차 렌더(최종)가 다시 실행됨 (멱등) # validate_engine_harness_gate.py 내부에서 2차 렌더(최종)가 다시 실행됨 (멱등)
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(pre-phase45) FAIL — 계속 진행" } if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(pre-phase45) FAIL — 계속 진행" }
# BLANK_CELL_AUDIT_V1 (1차 렌더 이후 실행 — 게이트 검증기에서 2차 재실행됨) # BLANK_CELL_AUDIT_V1 (1차 렌더 이후 실행 — 게이트 검증기에서 2차 재실행됨)
@@ -143,7 +143,7 @@ python .\tools\build_final_judgment_gate_v1.py --json $JsonPath --out .\Temp\fin
if ($LASTEXITCODE -ne 0) { Write-Warning "FINAL_JUDGMENT_GATE_V1 FAIL — 계속 진행" } if ($LASTEXITCODE -ne 0) { Write-Warning "FINAL_JUDGMENT_GATE_V1 FAIL — 계속 진행" }
# 2차 렌더 (final_judgment_table + investment_quality_headline 섹션 포함) # 2차 렌더 (final_judgment_table + investment_quality_headline 섹션 포함)
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase6-final) FAIL — 계속 진행" } if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase6-final) FAIL — 계속 진행" }
# VERDICT_CONSISTENCY_LOCK_V1 (render 이후 실행 — 최신 보고서 기준 검증) # VERDICT_CONSISTENCY_LOCK_V1 (render 이후 실행 — 최신 보고서 기준 검증)
@@ -165,7 +165,7 @@ python .\tools\build_canonical_metrics_v1.py
if ($LASTEXITCODE -ne 0) { Write-Warning "CANONICAL_METRICS_V1 FAIL — 계속 진행" } if ($LASTEXITCODE -ne 0) { Write-Warning "CANONICAL_METRICS_V1 FAIL — 계속 진행" }
# 3차 렌더 (canonical 값이 주입된 최신 보고서 생성) # 3차 렌더 (canonical 값이 주입된 최신 보고서 생성)
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase7-canonical) FAIL — 계속 진행" } if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase7-canonical) FAIL — 계속 진행" }
# CROSS_SECTION_CONSISTENCY_V1 — 교차섹션 정합성 게이트 (render 이후 실행) # CROSS_SECTION_CONSISTENCY_V1 — 교차섹션 정합성 게이트 (render 이후 실행)
+1 -1
View File
@@ -41,7 +41,7 @@ def _full_commands() -> list[list[str]]:
return [ return [
_cmd("tools/audit_repository_entropy_v1.py", "--root", ".", "--out", "runtime/baseline_manifest_v1.yaml"), _cmd("tools/audit_repository_entropy_v1.py", "--root", ".", "--out", "runtime/baseline_manifest_v1.yaml"),
*_release_commands(), *_release_commands(),
_cmd("tools/build_final_decision_packet_v4.py", "--src", "Temp/final_decision_packet_active.json", "--out", "Temp/final_decision_packet_v4.json"), ["dotnet", "run", "--project", str(ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "QuantEngine.Tools.csproj"), "--", "packet-v4", "--packet=Temp/final_decision_packet_active.json", "--out=Temp/final_decision_packet_v4.json"],
_cmd("tools/build_final_context_for_llm_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/final_context_for_llm_v4.yaml"), _cmd("tools/build_final_context_for_llm_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/final_context_for_llm_v4.yaml"),
_cmd("tools/build_number_provenance_ledger_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/number_provenance_ledger_v4.json"), _cmd("tools/build_number_provenance_ledger_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/number_provenance_ledger_v4.json"),
_cmd("tools/build_live_replay_separation_v2.py", "--hist", "Temp/proposal_evaluation_history.json", "--out", "Temp/live_replay_separation_v2.json"), _cmd("tools/build_live_replay_separation_v2.py", "--hist", "Temp/proposal_evaluation_history.json", "--out", "Temp/live_replay_separation_v2.json"),
+1 -1
View File
@@ -9,7 +9,7 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python .\tools\build_request_result_summary.py ` python .\tools\build_request_result_summary.py `
--gate .\Temp\engine_harness_gate_result.json ` --gate .\Temp\engine_harness_gate_result.json `
--out .\temp\request_result.txt --out .\Temp\request_result.txt
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Output "YOLO_FULL_CYCLE_OK" Write-Output "YOLO_FULL_CYCLE_OK"
+10 -10
View File
@@ -129,16 +129,16 @@ def main() -> int:
( (
"render_operational_report", "render_operational_report",
[ [
"python", "dotnet",
"tools/render_operational_report.py", "run",
"--json", "--project",
str(json_path), str(ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "QuantEngine.Tools.csproj"),
"--output", "--",
str(report_path), "report",
"--improvement-harness-json", f"--packet={ROOT / 'Temp' / 'final_decision_packet_active.json'}",
str(harness_json_path), f"--out={ROOT / 'Temp' / 'operational_report.json'}",
], ],
["REPORT RENDERED OK", "PREDICTION_IMPROVEMENT_HARNESS_EXPORTED"], ["operational_report.json"],
), ),
( (
"build_sector_trend_analysis_v1", "build_sector_trend_analysis_v1",
@@ -215,7 +215,7 @@ def main() -> int:
failed = True failed = True
# ── render 완료 후 blank_cell_audit 재실행 ───────────────────────────────── # ── render 완료 후 blank_cell_audit 재실행 ─────────────────────────────────
# render_operational_report.py(CHECK_12)가 최신 Phase 2B 주입으로 report를 갱신한 뒤 # .NET report builder가 최신 Phase 2B 주입으로 report를 갱신한 뒤
# blank_cell_audit_v1.py를 다시 실행해야 정확한 빈 셀 수를 반영한다. # blank_cell_audit_v1.py를 다시 실행해야 정확한 빈 셀 수를 반영한다.
# ps1에서 Phase 2B 도구 이전에 이미 한 번 실행됐지만 그것은 구버전 보고서 기준. # ps1에서 Phase 2B 도구 이전에 이미 한 번 실행됐지만 그것은 구버전 보고서 기준.
_bca_code, _ = _run([ _bca_code, _ = _run([
+2 -2
View File
@@ -29,7 +29,7 @@ CONTRACTS = [
{ {
"id": "final_decision_packet_active", "id": "final_decision_packet_active",
"file": "Temp/final_decision_packet_active.json", "file": "Temp/final_decision_packet_active.json",
"generator": "tools/build_packet_from_context_v1.py (inject_computed_harness 포함)", "generator": "src/dotnet/QuantEngine.Tools -- packet-v4",
"required_keys": ["formula_id", "meta", "canonical_metrics", "pass_100", "execution_readiness", "prediction"], "required_keys": ["formula_id", "meta", "canonical_metrics", "pass_100", "execution_readiness", "prediction"],
"non_null_keys": ["formula_id", "pass_100", "execution_readiness"], "non_null_keys": ["formula_id", "pass_100", "execution_readiness"],
"list_non_empty_keys": [], "list_non_empty_keys": [],
@@ -42,7 +42,7 @@ CONTRACTS = [
{ {
"id": "operational_report", "id": "operational_report",
"file": "Temp/operational_report.json", "file": "Temp/operational_report.json",
"generator": "tools/render_operational_report.py", "generator": "src/dotnet/QuantEngine.Tools -- report",
"required_keys": ["schema_version", "generated_at", "sections", "section_errors"], "required_keys": ["schema_version", "generated_at", "sections", "section_errors"],
"non_null_keys": ["sections"], "non_null_keys": ["sections"],
"list_non_empty_keys": ["sections"], "list_non_empty_keys": ["sections"],
+20 -7
View File
@@ -5,8 +5,6 @@ import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from jsonschema import Draft202012Validator
from operational_report_contract import REPORT_SECTION_ORDER from operational_report_contract import REPORT_SECTION_ORDER
@@ -69,11 +67,26 @@ def main() -> int:
print("OPERATIONAL_REPORT_JSON_FAIL: invalid schema file") print("OPERATIONAL_REPORT_JSON_FAIL: invalid schema file")
return 1 return 1
validator = Draft202012Validator(schema) if payload.get("schema_version") != schema.get("properties", {}).get("schema_version", {}).get("const"):
for error in validator.iter_errors(payload): errors.append("schema_version const mismatch")
pointer = "/".join(str(part) for part in error.absolute_path) if payload.get("source_json") != schema.get("properties", {}).get("source_json", {}).get("const"):
location = f" at {pointer}" if pointer else "" errors.append("source_json const mismatch")
errors.append(f"schema_error{location}: {error.message}")
sections = payload.get("sections")
if not isinstance(sections, list):
errors.append("sections: must be array")
sections = []
else:
for idx, section in enumerate(sections):
if not isinstance(section, dict):
errors.append(f"sections[{idx}]: must be object")
continue
if not isinstance(section.get("name"), str) or not section.get("name").strip():
errors.append(f"sections[{idx}]: missing name")
if not isinstance(section.get("title"), str) or not section.get("title").strip():
errors.append(f"sections[{idx}]: missing title")
if not isinstance(section.get("markdown"), str) or not section.get("markdown").startswith(f"## {section.get('title')}"):
errors.append(f"sections[{idx}]: markdown/title mismatch")
missing_top = REQUIRED_TOP_LEVEL_KEYS - set(payload) missing_top = REQUIRED_TOP_LEVEL_KEYS - set(payload)
if missing_top: if missing_top:
+17 -6
View File
@@ -62,13 +62,24 @@ class _CalcVisitor(ast.NodeVisitor):
def main() -> int: def main() -> int:
path = ROOT / "tools" / "render_operational_report.py" path = ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs"
text = read_text(path) text = read_text(path)
tree = ast.parse(text) if path.suffix.lower() == ".cs":
visitor = _CalcVisitor() calc_lines = []
visitor.source = text for idx, line in enumerate(text.splitlines(), start=1):
visitor.visit(tree) stripped = line.strip()
calc_lines = visitor.violations if not stripped or stripped.startswith("//"):
continue
if '"' in stripped or "'" in stripped:
continue
if any(token in stripped for token in [" + ", " - ", " * ", " / ", "Math.Round(", "Math.Min(", "Math.Max("]):
calc_lines.append({"line": str(idx), "text": stripped})
else:
tree = ast.parse(text)
visitor = _CalcVisitor()
visitor.source = text
visitor.visit(tree)
calc_lines = visitor.violations
result = { result = {
"formula_id": "RENDERER_NO_CALCULATION_V1", "formula_id": "RENDERER_NO_CALCULATION_V1",
"renderer_calculation_count": len(calc_lines), "renderer_calculation_count": len(calc_lines),
+2 -3
View File
@@ -9,10 +9,9 @@ from validate_renderer_no_calculation_v1 import main as validate_v1
def main() -> int: def main() -> int:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--renderer", default="tools/render_operational_report.py") ap.add_argument("--renderer", default="src/dotnet/QuantEngine.Tools/Program.cs")
args = ap.parse_args() args = ap.parse_args()
# v2 keeps the same static scan but allows explicit renderer path for future parity checks. # v2 keeps the same static scan but points at the canonical .NET renderer.
# The underlying implementation already validates the current canonical renderer.
_ = Path(args.renderer) _ = Path(args.renderer)
return validate_v1() return validate_v1()