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
@@ -3,6 +3,7 @@
@using QuantEngine.Core.Interfaces
@inject NavigationManager NavManager
@inject ISnackbar Snackbar
@inject IPostgresqlHistorySnapshotReader HistoryReader
<PageTitle>Quant Engine - Dashboard</PageTitle>
@@ -14,8 +15,8 @@
<MudCard Class="h-100">
<MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Active Positions</MudText>
<MudText Typo="Typo.h5" Class="mt-2">12</MudText>
<MudText Typo="Typo.caption" Class="mt-1">+2 since yesterday</MudText>
<MudText Typo="Typo.h5" Class="mt-2">@activePositions</MudText>
<MudText Typo="Typo.caption" Class="mt-1">from history snapshot</MudText>
</MudCardContent>
</MudCard>
</MudItem>
@@ -24,8 +25,8 @@
<MudCard Class="h-100">
<MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Portfolio Value</MudText>
<MudText Typo="Typo.h5" Class="mt-2">394.2M</MudText>
<MudText Typo="Typo.caption" Class="mt-1">KRW</MudText>
<MudText Typo="Typo.h5" Class="mt-2">@portfolioValueLabel</MudText>
<MudText Typo="Typo.caption" Class="mt-1">PostgreSQL snapshot</MudText>
</MudCardContent>
</MudCard>
</MudItem>
@@ -34,8 +35,8 @@
<MudCard Class="h-100">
<MudCardContent>
<MudText Typo="Typo.body2" Color="Color.Secondary">Signal Quality</MudText>
<MudText Typo="Typo.h5" Class="mt-2">84.5%</MudText>
<MudText Typo="Typo.caption" Class="mt-1">Win Rate (YTD)</MudText>
<MudText Typo="Typo.h5" Class="mt-2">@signalQualityLabel</MudText>
<MudText Typo="Typo.caption" Class="mt-1">decision_result_history</MudText>
</MudCardContent>
</MudCard>
</MudItem>
@@ -44,7 +45,7 @@
<MudCard Class="h-100">
<MudCardContent>
<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>
</MudCard>
</MudItem>
@@ -62,16 +63,16 @@
<MudCardContent>
<MudStack Spacing="2">
<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 Typo="Typo.body2">
<strong>Volatility:</strong> High (VIX equivalent)
<strong>Volatility:</strong> @volatilityLabel
</MudText>
<MudText Typo="Typo.body2">
<strong>Cash Position:</strong> 3.86% (Target: 15%)
<strong>Cash Position:</strong> @cashPositionLabel
</MudText>
<MudText Typo="Typo.body2">
<strong>Last Updated:</strong> @DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
<strong>Last Updated:</strong> @lastUpdatedLabel
</MudText>
</MudStack>
</MudCardContent>
@@ -89,15 +90,15 @@
<MudStack Spacing="2">
<MudText Typo="Typo.body2">
<strong>Database:</strong>
<MudChip Size="Size.Small" Color="Color.Success">Connected</MudChip>
<MudChip Size="Size.Small" Color="Color.Success">@databaseLabel</MudChip>
</MudText>
<MudText Typo="Typo.body2">
<strong>GAS Feed:</strong>
<MudChip Size="Size.Small" Color="Color.Success">Active</MudChip>
<strong>DB History Feed:</strong>
<MudChip Size="Size.Small" Color="Color.Success">@historyFeedLabel</MudChip>
</MudText>
<MudText Typo="Typo.body2">
<strong>Signal Generator:</strong>
<MudChip Size="Size.Small" Color="Color.Info">Running</MudChip>
<MudChip Size="Size.Small" Color="Color.Info">@signalGeneratorLabel</MudChip>
</MudText>
<MudText Typo="Typo.body2">
<strong>API Uptime:</strong> 99.8%
@@ -120,30 +121,30 @@
<MudStack Row="true" Spacing="3">
<MudItem xs="12" sm="6" md="4">
<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 xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Sharpe Ratio</strong></MudText>
<MudText Typo="Typo.h6">1.85</MudText>
<MudText Typo="Typo.h6">@sharpeLabel</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<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>
</MudStack>
<MudStack Row="true" Spacing="3">
<MudItem xs="12" sm="6" md="4">
<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 xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Profit Factor</strong></MudText>
<MudText Typo="Typo.h6">1.95</MudText>
<MudText Typo="Typo.h6">@profitFactorLabel</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.body2"><strong>Trades This Month</strong></MudText>
<MudText Typo="Typo.h6">24</MudText>
<MudText Typo="Typo.h6">@tradesThisMonthLabel</MudText>
</MudItem>
</MudStack>
</MudStack>
@@ -177,8 +178,8 @@
</MudTd>
<MudTd>
@{
var progress = context["Progress"].ToString().Replace("%", "");
var progressValue = int.TryParse(progress, out var val) ? val : 0;
var progress = context.TryGetValue("Progress", out var p) ? p?.ToString() ?? string.Empty : string.Empty;
var progressValue = int.TryParse(progress.Replace("%", ""), out var val) ? val : 0;
}
<MudProgressLinear Value="@progressValue" Size="Size.Small" />
</MudTd>
@@ -225,69 +226,85 @@
</MudCard>
@code {
private List<Dictionary<string, object>> algorithmPhases = new()
{
new() { { "Phase", "P0" }, { "Name", "Falsehood Elimination" }, { "Status", "Calibrated" }, { "Progress", "100%" } },
new() { { "Phase", "P1" }, { "Name", "Unified Execution Authority" }, { "Status", "Calibrated" }, { "Progress", "100%" } },
new() { { "Phase", "P2" }, { "Name", "Live Outcome Ledger" }, { "Status", "Running" }, { "Progress", "30%" } },
new() { { "Phase", "P3" }, { "Name", "Stop Loss Taxonomy" }, { "Status", "Running" }, { "Progress", "60%" } },
new() { { "Phase", "P4" }, { "Name", "Unified Routing" }, { "Status", "Deployed" }, { "Progress", "85%" } },
new() { { "Phase", "P5" }, { "Name", "Anti-Late Entry" }, { "Status", "Active" }, { "Progress", "75%" } },
new() { { "Phase", "P6" }, { "Name", "Cash Preservation" }, { "Status", "Active" }, { "Progress", "80%" } }
};
private List<Dictionary<string, object>> recentSignals = new()
{
new()
{
{ "Timestamp", "2026-06-25 14:35" },
{ "Ticker", "000660" },
{ "Signal", "BUY" },
{ "Score", "78" },
{ "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" }
}
};
private List<Dictionary<string, object>> algorithmPhases = new();
private List<Dictionary<string, object>> recentSignals = new();
private string activePositions = "0";
private string portfolioValueLabel = "n/a";
private string signalQualityLabel = "n/a";
private string dbStatusLabel = "Pending";
private string marketRegimeLabel = "PENDING";
private string volatilityLabel = "n/a";
private string cashPositionLabel = "n/a";
private string lastUpdatedLabel = "n/a";
private string databaseLabel = "Pending";
private string historyFeedLabel = "Pending";
private string signalGeneratorLabel = "Pending";
private string ytdReturnLabel = "n/a";
private string sharpeLabel = "n/a";
private string maxDrawdownLabel = "n/a";
private string winRateLabel = "n/a";
private string profitFactorLabel = "n/a";
private string tradesThisMonthLabel = "0";
protected override async Task OnInitializedAsync()
{
// 초기화 작업
await Task.CompletedTask;
await LoadHistoryAsync();
}
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.Repositories;
using QuantEngine.Core.Interfaces;
using QuantEngine.Application.Services;
using System.Text.Json;
using MudBlazor.Services;
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;";
builder.Services.AddSingleton<IDbConnectionFactory>(new DbConnectionFactory(connectionString));
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
builder.Services.AddScoped<HistoryIngestionService>();
builder.Services.AddHttpClient();
var app = builder.Build();
@@ -40,5 +45,48 @@ app.MapStaticAssets();
app.MapRazorComponents<App>()
.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();