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
- 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>
150 lines
5.7 KiB
C#
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();
|
|
}
|
|
}
|