security: hard-disable all KIS trading paths (AEG-X-016)

Blocks submit, status, cancel, and settlement before HTTP or database writes and removes the KIS polling recurring job. Evidence: concrete adapter test 1/1 passed with zero HTTP calls. WBS remains IN_PROGRESS pending endpoint/startup override evidence.
This commit is contained in:
2026-08-09 02:30:39 +09:00
parent ec80337389
commit eb59cae8e3
8 changed files with 113 additions and 3 deletions
+2 -2
View File
@@ -398,8 +398,8 @@ try { RecurringJob.AddOrUpdate<OpenDartDailyBatchJob>("opendart-daily-batch", jo
try { RecurringJob.AddOrUpdate<GenerateDailyRecommendationJob>("daily-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ daily-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ daily-recommendation error"); }
try { RecurringJob.AddOrUpdate<GenerateWeeklyRecommendationJob>("weekly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * 6", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ weekly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ weekly-recommendation error"); }
try { RecurringJob.AddOrUpdate<GenerateMonthlyRecommendationJob>("monthly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 1 * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ monthly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ monthly-recommendation error"); }
// DEBT-027: without this, PollTradeStatusHandler/ConfirmSettlementHandler were reachable via DI but never invoked by anything.
try { RecurringJob.AddOrUpdate<KArtSell.Host.Jobs.TradeStatusPollingJob>("trade-status-polling", job => job.ExecuteAsync(CancellationToken.None), "*/2 * * * *", new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); logger.LogInformation("✅ trade-status-polling registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ trade-status-polling error"); }
RecurringJob.RemoveIfExists("trade-status-polling");
logger.LogWarning("KIS trade-status-polling removed: KIS trading is hard-disabled.");
logger.LogInformation("🎯 Hangfire background jobs initialization complete");
@@ -25,6 +25,17 @@ public class KisTradeExecutionException : Exception
}
}
public sealed class KisTradingDisabledException : InvalidOperationException
{
public KisTradingDisabledException()
: base("KIS trading is hard-disabled. No buy, sell, cancel, status, or settlement call is permitted.") { }
}
public static class KisTradingHardOff
{
public static void EnsureDisabled() => throw new KisTradingDisabledException();
}
public interface IKisTradeExecutionService
{
Task<(string OrderId, JsonElement Response)> ExecuteTradeAsync(Guid tradeId, int quantity, decimal limitPrice, Guid correlationId, CancellationToken ct = default);
@@ -56,6 +67,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new
{
symbol = "US0100",
@@ -120,6 +132,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var request = new HttpRequestMessage(HttpMethod.Get, $"{KisApiBase}/orders/{kisOrderId}");
request.Headers.Add("X-Correlation-ID", correlationId.ToString());
@@ -159,6 +172,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new { reason = reason };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
@@ -195,6 +209,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new { confirm = true };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
@@ -42,6 +42,7 @@ public class SubmitTradeHandler
public async Task<Guid> HandleAsync(SubmitTradeCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = Trade.Create(command.SellDecisionId, command.Quantity, command.CorrelationId, _clock.UtcNow.UtcDateTime);
await _sql.InsertTradeAsync(trade, ct);
_logger.LogInformation("Created trade: {TradeId}", trade.Id);
@@ -127,6 +128,7 @@ public class PollTradeStatusHandler
public async Task HandleAsync(PollTradeStatusCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
if (trade == null)
{
@@ -222,6 +224,7 @@ public class ConfirmSettlementHandler
public async Task HandleAsync(ConfirmSettlementCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
if (trade == null)
{