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 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>(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 ex) { 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 latest) { result.ShortTurnoverShare = CoerceFloat(latest.GetValueOrDefault("ssts_vol_rlim")); } result.ShortSaleStatus = "OK"; result.ShortSaleRaw = (Dictionary?)rows.FirstOrDefault() ?? new(); } catch (Exception ex) { 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 payload, params string[] keys) { var stack = new Stack(); stack.Push(payload); while (stack.Count > 0) { var item = stack.Pop(); if (item is Dictionary 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 ExtractObject(Dictionary payload, string key) { if (payload.TryGetValue(key, out var value) && value is Dictionary dict) return dict; if (value is JsonElement elem && elem.ValueKind == JsonValueKind.Object) return JsonSerializer.Deserialize>(elem.GetRawText()) ?? new(); return new(); } private static List ExtractArray(Dictionary payload, string key) { if (payload.TryGetValue(key, out var value)) { if (value is List list) return list; if (value is JsonElement elem && elem.ValueKind == JsonValueKind.Array) return JsonSerializer.Deserialize>(elem.GetRawText()) ?? new(); } return new(); } }