From 82e18a9a22e697449bc1e8f50635de02ffe7b018 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 12 Jul 2026 14:18:44 +0900 Subject: [PATCH] feat(web): implement KisApiClient request throttling to prevent KIS OpenAPI rate limits --- .../Services/KisApiClient.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs b/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs index 25d8af39..13f93dd9 100644 --- a/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs +++ b/src/dotnet/QuantEngine.Infrastructure/Services/KisApiClient.cs @@ -32,6 +32,8 @@ public class KisApiClient : IKisApiClient private readonly ITokenCache _tokenCache; private readonly ILogger _logger; private static readonly ConcurrentDictionary TokenLocks = new(); + private readonly SemaphoreSlim _rateLimitSemaphore = new(1, 1); + private DateTime _lastRequestTime = DateTime.MinValue; public KisApiClient(HttpClient httpClient, ITokenCache tokenCache, ILogger logger) { @@ -152,6 +154,7 @@ public class KisApiClient : IKisApiClient foreach (var header in headers) request.Headers.Add(header.Key, header.Value); + await ApplyRateLimitDelayAsync(account); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); @@ -195,6 +198,7 @@ public class KisApiClient : IKisApiClient { try { + await ApplyRateLimitDelayAsync(creds.Account); var response = await _httpClient.PostAsJsonAsync( $"{creds.Domain}/oauth2/tokenP", tokenRequest @@ -318,4 +322,29 @@ public class KisApiClient : IKisApiClient return null; } } + + private async Task ApplyRateLimitDelayAsync(string account) + { + await _rateLimitSemaphore.WaitAsync(); + try + { + var mode = account.Contains("real", StringComparison.OrdinalIgnoreCase) ? "real" : "mock"; + int minIntervalMs = mode == "real" ? 150 : 400; + + var now = DateTime.UtcNow; + var elapsed = (now - _lastRequestTime).TotalMilliseconds; + if (elapsed < minIntervalMs) + { + var delay = minIntervalMs - (int)elapsed; + _logger.LogDebug("Rate limit throttling: delaying for {Delay}ms (Mode: {Mode})", delay, mode); + await Task.Delay(delay); + } + + _lastRequestTime = DateTime.UtcNow; + } + finally + { + _rateLimitSemaphore.Release(); + } + } }