Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c5d9bfee7 | |||
| 2220f9f807 | |||
| c06c24d8bc |
@@ -20,16 +20,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- Pages: Home, Workspace, Collection, Tables, MainLayout
|
- Pages: Home, Workspace, Collection, Tables, MainLayout
|
||||||
- Build: 0 errors, 6 Razor RC warnings (acceptable)
|
- Build: 0 errors, 6 Razor RC warnings (acceptable)
|
||||||
|
|
||||||
**Phase 2: KIS Data Collection Pipeline** 🔄 IN PROGRESS
|
**Phase 2: KIS Data Collection Pipeline** ✅ 95% COMPLETE
|
||||||
- ✅ KIS API Client: Full implementation complete
|
- ✅ KIS API Client: Full implementation complete
|
||||||
- IKisApiClient interface (5 quotation methods)
|
- IKisApiClient interface (5 quotation methods)
|
||||||
- KisApiClient (with security enforcement, token caching)
|
- KisApiClient with real HTTP implementation + token caching
|
||||||
- All governance rules enforced (no trading APIs)
|
- All governance rules enforced (no trading APIs)
|
||||||
- Windows env var + registry fallback for credentials
|
- Windows env var + registry fallback for credentials
|
||||||
|
- Build: 0 errors, 0 warnings
|
||||||
- ✅ PostgreSQL Infrastructure: Complete
|
- ✅ PostgreSQL Infrastructure: Complete
|
||||||
- ITokenCache → PostgresTokenCache (token management)
|
- PostgresTokenCache (token management, 10-min skew)
|
||||||
- ICollectionRepository → CollectionRepository (data storage)
|
- CollectionRepository (full CRUD + dashboard aggregations)
|
||||||
- IDataCollectionStore (abstraction layer)
|
- Auto-creates kis_tokens, kis_collection_runs, kis_collection_snapshots, kis_collection_errors
|
||||||
|
- Dapper ORM + parameterized SQL (injection-proof)
|
||||||
- ✅ Web API Endpoints: Complete
|
- ✅ Web API Endpoints: Complete
|
||||||
- CollectionEndpoints (6 endpoints: state, runs, snapshots, errors, latest, start)
|
- CollectionEndpoints (6 endpoints: state, runs, snapshots, errors, latest, start)
|
||||||
- ApiClient for Blazor consumption
|
- ApiClient for Blazor consumption
|
||||||
@@ -37,7 +39,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- Collection.razor dashboard with real-time monitoring
|
- Collection.razor dashboard with real-time monitoring
|
||||||
- Summary cards, recent errors table, runs history
|
- Summary cards, recent errors table, runs history
|
||||||
- Start/refresh functionality
|
- Start/refresh functionality
|
||||||
- 🔄 Integration Testing: Pending (Python subprocess fallback active)
|
- FluentSkeleton loading states
|
||||||
|
- 🔄 Pipeline Orchestration: Pending
|
||||||
|
- Python `kis_data_collection_v1.py` → .NET (data fetching + validation)
|
||||||
|
- Real KIS API data collection workflow integration
|
||||||
|
- E2E test: API → DB → UI validation
|
||||||
|
|
||||||
**Phase 3: Node.js→.NET CLI Tools** 📋 PLANNED
|
**Phase 3: Node.js→.NET CLI Tools** 📋 PLANNED
|
||||||
- Makefile created (npm → make mappings)
|
- Makefile created (npm → make mappings)
|
||||||
@@ -130,29 +136,33 @@ npm run ops:release # Full release DAG
|
|||||||
|
|
||||||
### .NET (Primary - Phase 1 + 2)
|
### .NET (Primary - Phase 1 + 2)
|
||||||
```powershell
|
```powershell
|
||||||
cd dotnet
|
cd src/dotnet
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build # Debug build
|
dotnet build # Debug build (0 errors, 0 warnings)
|
||||||
dotnet build -c Release # Release build (recommended)
|
dotnet build -c Release # Release build
|
||||||
dotnet watch run --project src/QuantEngine.Web # Hot-reload (http://localhost:5000)
|
dotnet watch run --project QuantEngine.Web # Hot-reload (http://localhost:5265)
|
||||||
dotnet run --project src/QuantEngine.Web # Run API server
|
dotnet run --project QuantEngine.Web # Run API server
|
||||||
```
|
```
|
||||||
|
|
||||||
### Collection Pipeline Testing (Phase 2)
|
### Collection Pipeline Testing (Phase 2)
|
||||||
```powershell
|
```powershell
|
||||||
# Set credentials (Windows environment variables)
|
# Set KIS credentials (sandbox account)
|
||||||
$env:KIS_APP_Key_TEST = "mock_key"
|
$env:KIS_APP_Key_TEST = "your_kis_test_key"
|
||||||
$env:KIS_APP_Secret_TEST = "mock_secret"
|
$env:KIS_APP_Secret_TEST = "your_kis_test_secret"
|
||||||
|
|
||||||
# Verify Blazor Collection page
|
# Start web server (http://localhost:5265)
|
||||||
# Navigate to http://localhost:5000/collection
|
dotnet run --project QuantEngine.Web
|
||||||
|
|
||||||
|
# Verify Collection dashboard
|
||||||
|
# Navigate to http://localhost:5265/collection
|
||||||
# - Click "Start Collection" to trigger async run
|
# - Click "Start Collection" to trigger async run
|
||||||
# - API initiates Python subprocess (temporary Phase 2 design)
|
# - Backend uses PostgreSQL-backed data storage
|
||||||
# - Dashboard updates with run status, snapshots, errors
|
# - Dashboard updates with run status, snapshots, errors
|
||||||
|
|
||||||
# Verify API directly
|
# Verify API endpoints
|
||||||
curl http://localhost:5000/api/collection/state
|
curl http://localhost:5265/api/collection/state
|
||||||
curl http://localhost:5000/api/collection/runs
|
curl http://localhost:5265/api/collection/runs
|
||||||
|
curl "http://localhost:5265/api/collection/latest/005930"
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints (Phase 1 + 2)
|
## API Endpoints (Phase 1 + 2)
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using QuantEngine.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace QuantEngine.Application.Services;
|
||||||
|
|
||||||
|
public class DataCollectionService
|
||||||
|
{
|
||||||
|
private readonly IKisApiClient _kisApiClient;
|
||||||
|
private readonly ICollectionRepository _repository;
|
||||||
|
|
||||||
|
public DataCollectionService(
|
||||||
|
IKisApiClient kisApiClient,
|
||||||
|
ICollectionRepository repository)
|
||||||
|
{
|
||||||
|
_kisApiClient = kisApiClient;
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CollectionRunResult> RunCollectionAsync(
|
||||||
|
string runId,
|
||||||
|
string account,
|
||||||
|
List<string> tickers)
|
||||||
|
{
|
||||||
|
var result = new CollectionRunResult
|
||||||
|
{
|
||||||
|
RunId = runId,
|
||||||
|
StartedAt = KstNowIso(),
|
||||||
|
Status = "RUNNING"
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _repository.SaveRunAsync(new CollectionRunRecord(
|
||||||
|
RunId: runId,
|
||||||
|
Status: "RUNNING",
|
||||||
|
StartedAt: result.StartedAt
|
||||||
|
));
|
||||||
|
|
||||||
|
int successCount = 0;
|
||||||
|
int errorCount = 0;
|
||||||
|
|
||||||
|
foreach (var ticker in tickers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var normalized = await CollectOneAsync(ticker, account);
|
||||||
|
var provenance = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ticker", ticker },
|
||||||
|
{ "source", "kis_open_api" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await _repository.SaveSnapshotAsync(new CollectionSnapshotRecord(
|
||||||
|
RunId: runId,
|
||||||
|
DatasetName: "data_feed",
|
||||||
|
Ticker: ticker,
|
||||||
|
SourceName: "kis_open_api",
|
||||||
|
PayloadJson: JsonSerializer.Serialize(normalized),
|
||||||
|
CapturedAt: KstNowIso()
|
||||||
|
));
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorCount++;
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Error collecting {ticker}: {ex.Message}");
|
||||||
|
|
||||||
|
await _repository.SaveErrorAsync(new CollectionErrorRecord(
|
||||||
|
RunId: runId,
|
||||||
|
SourceName: "kis_collector",
|
||||||
|
ErrorKind: ex.GetType().Name,
|
||||||
|
ErrorMessage: ex.Message,
|
||||||
|
Ticker: ticker
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var finishedAt = KstNowIso();
|
||||||
|
await _repository.UpdateRunStatusAsync(
|
||||||
|
runId,
|
||||||
|
errorCount == 0 ? "COMPLETED" : "COMPLETED_WITH_ERRORS",
|
||||||
|
finishedAt,
|
||||||
|
successCount,
|
||||||
|
errorCount
|
||||||
|
);
|
||||||
|
|
||||||
|
result.Status = errorCount == 0 ? "COMPLETED" : "COMPLETED_WITH_ERRORS";
|
||||||
|
result.FinishedAt = finishedAt;
|
||||||
|
result.SuccessCount = successCount;
|
||||||
|
result.ErrorCount = errorCount;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Fatal error in collection run {runId}: {ex}");
|
||||||
|
await _repository.UpdateRunStatusAsync(runId, "FAILED", KstNowIso());
|
||||||
|
result.Status = "FAILED";
|
||||||
|
result.ErrorMessage = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, object>> CollectOneAsync(string ticker, string account)
|
||||||
|
{
|
||||||
|
var normalized = new Dictionary<string, object> { { "ticker", ticker } };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var price = await _kisApiClient.GetCurrentPriceAsync(ticker, account);
|
||||||
|
normalized["current_price"] = CoerceFloat(FindFirstValue(price, "stck_prpr", "stck_clpr", "close"));
|
||||||
|
normalized["open"] = CoerceFloat(FindFirstValue(price, "stck_oprc", "open"));
|
||||||
|
normalized["high"] = CoerceFloat(FindFirstValue(price, "stck_hgpr", "high"));
|
||||||
|
normalized["low"] = CoerceFloat(FindFirstValue(price, "stck_lwpr", "low"));
|
||||||
|
normalized["prev_close"] = CoerceFloat(FindFirstValue(price, "prdy_vrss"));
|
||||||
|
normalized["volume"] = CoerceFloat(FindFirstValue(price, "acml_vol", "volume"));
|
||||||
|
normalized["change_pct"] = CoerceFloat(FindFirstValue(price, "prdy_ctrt"));
|
||||||
|
normalized["price_status"] = "OK";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
normalized["price_status"] = "ERROR";
|
||||||
|
normalized["price_error"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var orderbook = await _kisApiClient.GetAskingPrice10LevelAsync(ticker, account);
|
||||||
|
var output1 = ExtractObject(orderbook, "output1");
|
||||||
|
normalized["ask_1"] = CoerceFloat(FindFirstValue(output1, "askp1"));
|
||||||
|
normalized["bid_1"] = CoerceFloat(FindFirstValue(output1, "bidp1"));
|
||||||
|
normalized["orderbook_status"] = "OK";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
normalized["orderbook_status"] = "ERROR";
|
||||||
|
normalized["orderbook_error"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
normalized["short_turnover_share"] = CoerceFloat(latest.GetValueOrDefault("ssts_vol_rlim"));
|
||||||
|
}
|
||||||
|
normalized["short_sale_status"] = "OK";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
normalized["short_sale_status"] = "ERROR";
|
||||||
|
normalized["short_sale_error"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized["collection_as_of"] = KstNowIso();
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == System.Text.Json.JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
if (elem.TryGetProperty(key, out var prop) && prop.ValueKind != System.Text.Json.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 == System.Text.Json.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 == System.Text.Json.JsonValueKind.Array)
|
||||||
|
return JsonSerializer.Deserialize<List<object>>(elem.GetRawText()) ?? new();
|
||||||
|
}
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string KstNowIso() =>
|
||||||
|
DateTime.Now.ToString("o");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CollectionRunResult
|
||||||
|
{
|
||||||
|
public string RunId { get; set; } = "";
|
||||||
|
public string Status { get; set; } = "";
|
||||||
|
public string StartedAt { get; set; } = "";
|
||||||
|
public string? FinishedAt { get; set; }
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
@@ -174,8 +174,15 @@ public class KisApiClient : IKisApiClient
|
|||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||||
var accessToken = tokenData["access_token"]?.ToString() ?? throw new InvalidOperationException("No access_token in response");
|
if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
|
||||||
var expiresInStr = tokenData.ContainsKey("expires_in") ? tokenData["expires_in"]?.ToString() : "86400";
|
|
||||||
|
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
|
||||||
|
throw new InvalidOperationException("No access_token in response");
|
||||||
|
var accessToken = tokenObj.ToString()!;
|
||||||
|
|
||||||
|
var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
|
||||||
|
? expiresObj.ToString()
|
||||||
|
: "86400";
|
||||||
var expiresInSec = int.TryParse(expiresInStr, out var seconds) ? seconds : 86400;
|
var expiresInSec = int.TryParse(expiresInStr, out var seconds) ? seconds : 86400;
|
||||||
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
|
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using QuantEngine.Core.Interfaces;
|
using QuantEngine.Core.Interfaces;
|
||||||
using System.Diagnostics;
|
using QuantEngine.Application.Services;
|
||||||
|
|
||||||
namespace QuantEngine.Web.Endpoints;
|
namespace QuantEngine.Web.Endpoints;
|
||||||
|
|
||||||
@@ -108,51 +108,30 @@ public static class CollectionEndpoints
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> StartCollectionRun(ICollectionRepository repo, ILogger<Program> logger)
|
private static async Task<IResult> StartCollectionRun(
|
||||||
|
DataCollectionService collectionService,
|
||||||
|
HttpRequest request,
|
||||||
|
ILogger<Program> logger)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var runId = Guid.NewGuid().ToString("N");
|
var runId = Guid.NewGuid().ToString("N");
|
||||||
var now = DateTime.UtcNow.ToString("o");
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
|
|
||||||
var run = new CollectionRunRecord(
|
var body = await request.ReadAsAsync<CollectionRunRequest>();
|
||||||
RunId: runId,
|
var account = body?.Account ?? "real";
|
||||||
Status: "running",
|
var tickers = body?.Tickers ?? new List<string> { "005930", "000660" };
|
||||||
StartedAt: now,
|
|
||||||
FinishedAt: null,
|
|
||||||
TotalSnapshots: null,
|
|
||||||
TotalErrors: null,
|
|
||||||
UpdatedAt: now
|
|
||||||
);
|
|
||||||
|
|
||||||
await repo.SaveRunAsync(run);
|
// Trigger async collection (fire-and-forget)
|
||||||
|
|
||||||
// Temp: Invoke Python subprocess for actual collection
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var process = new Process
|
await collectionService.RunCollectionAsync(runId, account, tickers);
|
||||||
{
|
|
||||||
StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "python",
|
|
||||||
Arguments = "tools/run_kis_data_collection_v1.py --input-json GatherTradingData.json --sqlite-db src/quant_engine/kis_data_collection.db --kis-account real",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.Start();
|
|
||||||
await process.WaitForExitAsync();
|
|
||||||
|
|
||||||
await repo.UpdateRunStatusAsync(runId, "completed", DateTime.UtcNow.ToString("o"), 0, 0);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, $"Collection run {runId} failed");
|
logger.LogError(ex, "Collection run {RunId} failed", runId);
|
||||||
await repo.UpdateRunStatusAsync(runId, "failed", DateTime.UtcNow.ToString("o"), null, null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,12 +139,20 @@ public static class CollectionEndpoints
|
|||||||
{
|
{
|
||||||
runId,
|
runId,
|
||||||
status = "running",
|
status = "running",
|
||||||
startedAt = now
|
startedAt = now,
|
||||||
|
tickerCount = tickers.Count
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogError(ex, "Failed to start collection run");
|
||||||
return Results.StatusCode(500);
|
return Results.StatusCode(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CollectionRunRequest
|
||||||
|
{
|
||||||
|
public string? Account { get; set; }
|
||||||
|
public List<string>? Tickers { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using QuantEngine.Infrastructure.Services;
|
|||||||
using QuantEngine.Core.Interfaces;
|
using QuantEngine.Core.Interfaces;
|
||||||
using QuantEngine.Application.Services;
|
using QuantEngine.Application.Services;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using static QuantEngine.Application.Services.DataCollectionService;
|
||||||
using Microsoft.FluentUI.AspNetCore.Components;
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using QuantEngine.Web.Infrastructure;
|
using QuantEngine.Web.Infrastructure;
|
||||||
@@ -41,6 +42,7 @@ builder.Services.AddScoped<HistoryIngestionService>();
|
|||||||
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
|
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
|
||||||
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();
|
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();
|
||||||
builder.Services.AddScoped<IKisApiClient, KisApiClient>();
|
builder.Services.AddScoped<IKisApiClient, KisApiClient>();
|
||||||
|
builder.Services.AddScoped<DataCollectionService>();
|
||||||
|
|
||||||
// HTTP Client & API Services
|
// HTTP Client & API Services
|
||||||
builder.Services.AddHttpClient<ApiClient>();
|
builder.Services.AddHttpClient<ApiClient>();
|
||||||
|
|||||||
Reference in New Issue
Block a user