Files
QuantEngineByItz/src/dotnet/QuantEngine.Application/Services/KisApiPriceSource.cs
T
kjh2064 668f109b01
Merge to Main - Full Pipeline / Stage 1: Fast Gates (push) Failing after 5s
Merge to Main - Full Pipeline / Stage 2: Critical Gates (push) Has been skipped
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
Merge to Main - Full Pipeline / Stage 4: Build and Package (push) Has been skipped
Merge to Main - Full Pipeline / Stage 5: Deploy to Production (push) Has been skipped
Merge to Main - Full Pipeline / Pipeline Summary (push) Successful in 1s
Merge to Main - Full Pipeline / Stage 3: Integration Tests (push) Has been skipped
fix: Remove compiler warnings in Application & Web
- KisApiPriceSource.cs: Use discard pattern for unused exception variables
- Monitoring/Index.cshtml: Handle nullable TotalErrors with null coalescing

Build now passes with 0 code-level warnings (6 remaining NuGet compatibility warnings are harmless).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-11 20:49:43 +09:00

150 lines
5.7 KiB
C#

using System.Text.Json;
using QuantEngine.Core.Interfaces;
using QuantEngine.Core.Models;
namespace QuantEngine.Application.Services;
public class KisApiPriceSource : IPriceSource
{
private readonly IKisApiClient _kisApiClient;
public string SourceName => "kis_open_api";
public KisApiPriceSource(IKisApiClient kisApiClient)
{
_kisApiClient = kisApiClient;
}
public async Task<PriceSourceResult> GetPriceDataAsync(string ticker, string account)
{
try
{
var result = new PriceSourceResult { Status = "OK", Source = "kis", Account = account };
// Get current price
try
{
var price = await _kisApiClient.GetCurrentPriceAsync(ticker, account);
result.CurrentPrice = CoerceFloat(FindFirstValue(price, "stck_prpr", "stck_clpr", "close"));
result.Open = CoerceFloat(FindFirstValue(price, "stck_oprc", "open"));
result.High = CoerceFloat(FindFirstValue(price, "stck_hgpr", "high"));
result.Low = CoerceFloat(FindFirstValue(price, "stck_lwpr", "low"));
result.PrevClose = CoerceFloat(FindFirstValue(price, "prdy_vrss"));
result.Volume = CoerceFloat(FindFirstValue(price, "acml_vol", "volume"));
result.ChangePct = CoerceFloat(FindFirstValue(price, "prdy_ctrt"));
result.PriceStatus = "OK";
result.CurrentPriceRaw = JsonSerializer.Deserialize<Dictionary<string, object>>(JsonSerializer.Serialize(price)) ?? new();
}
catch (Exception ex)
{
result.PriceStatus = "ERROR";
result.Error = ex.Message;
}
// Get orderbook
try
{
var orderbook = await _kisApiClient.GetAskingPrice10LevelAsync(ticker, account);
var output1 = ExtractObject(orderbook, "output1");
result.Ask1 = CoerceFloat(output1.GetValueOrDefault("askp1"));
result.Bid1 = CoerceFloat(output1.GetValueOrDefault("bidp1"));
result.OrderbookStatus = "OK";
result.OrderbookRaw = output1;
}
catch (Exception)
{
result.OrderbookStatus = "ERROR";
}
// Get short sale
try
{
var start = DateTime.Now.AddDays(-10).ToString("yyyyMMdd");
var end = DateTime.Now.ToString("yyyyMMdd");
var shortSale = await _kisApiClient.GetDailyShortSaleAsync(ticker, start, end, account);
var rows = ExtractArray(shortSale, "output2");
if (rows.Count > 0 && rows[0] is Dictionary<string, object> latest)
{
result.ShortTurnoverShare = CoerceFloat(latest.GetValueOrDefault("ssts_vol_rlim"));
}
result.ShortSaleStatus = "OK";
result.ShortSaleRaw = (Dictionary<string, object>?)rows.FirstOrDefault() ?? new();
}
catch (Exception)
{
result.ShortSaleStatus = "ERROR";
}
return result;
}
catch (Exception ex)
{
return new PriceSourceResult { Status = "ERROR", Error = ex.Message, Source = "kis", Account = account };
}
}
private static object? FindFirstValue(Dictionary<string, object> payload, params string[] keys)
{
var stack = new Stack<object>();
stack.Push(payload);
while (stack.Count > 0)
{
var item = stack.Pop();
if (item is Dictionary<string, object> dict)
{
foreach (var key in keys)
{
if (dict.TryGetValue(key, out var value) && value != null && !string.IsNullOrEmpty(value.ToString()))
return value;
}
foreach (var value in dict.Values)
if (value != null) stack.Push(value);
}
else if (item is JsonElement elem && elem.ValueKind == JsonValueKind.Object)
{
foreach (var key in keys)
{
if (elem.TryGetProperty(key, out var prop) && prop.ValueKind != JsonValueKind.Null)
return prop;
}
foreach (var prop in elem.EnumerateObject())
stack.Push(prop.Value);
}
}
return null;
}
private static double? CoerceFloat(object? value)
{
if (value == null || string.IsNullOrEmpty(value.ToString()))
return null;
try
{
var str = value.ToString()?.Replace(",", "").Replace("%", "") ?? "";
return double.TryParse(str, out var d) ? d : null;
}
catch { return null; }
}
private static Dictionary<string, object> ExtractObject(Dictionary<string, object> payload, string key)
{
if (payload.TryGetValue(key, out var value) && value is Dictionary<string, object> dict)
return dict;
if (value is JsonElement elem && elem.ValueKind == JsonValueKind.Object)
return JsonSerializer.Deserialize<Dictionary<string, object>>(elem.GetRawText()) ?? new();
return new();
}
private static List<object> ExtractArray(Dictionary<string, object> payload, string key)
{
if (payload.TryGetValue(key, out var value))
{
if (value is List<object> list) return list;
if (value is JsonElement elem && elem.ValueKind == JsonValueKind.Array)
return JsonSerializer.Deserialize<List<object>>(elem.GetRawText()) ?? new();
}
return new();
}
}