feat: .NET 운영 리포트 렌더러와 CI 경로 전환
- operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다. - CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다. - legacy Python 렌더러는 비운영으로 명시했습니다.
This commit is contained in:
@@ -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
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Web", "QuantEng
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "QuantEngine.Core.Tests\QuantEngine.Core.Tests.csproj", "{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user