Fixed database price history caching issue.

This commit is contained in:
caspar 2026-03-19 07:11:20 -04:00
parent f9ba5369bd
commit df759d0ead
4 changed files with 121 additions and 1 deletions

View File

@ -15,6 +15,20 @@ namespace Backend.Controller
_marketDataService = marketDataService;
}
[HttpGet("symbol-check/{symbol}")]
public IActionResult CheckSymbol(string symbol)
{
try
{
var (found, message) = _marketDataService.CheckSymbol(symbol.ToUpperInvariant());
return Ok(new { found, message });
}
catch (Exception ex)
{
return StatusCode(500, new { error = ex.ToString() });
}
}
[HttpGet("live/{symbol}")]
public IActionResult GetLive(string symbol)
{

View File

@ -29,6 +29,9 @@ namespace Backend.MarketDataRequest
private readonly ConcurrentDictionary<int, FundamentalsEquitySnapshot> _fundamentalsSnapshotsByRequest = new();
private readonly ConcurrentDictionary<int, TaskCompletionSource<FundamentalsEquitySnapshot>> _fundamentalsRequests = new();
// Symbol lookup data
private readonly ConcurrentDictionary<int, TaskCompletionSource<ContractDescription[]>> _symbolLookupRequests = new();
public IBGatewayControl()
{
Client = new EClientSocket(this, _signal);
@ -160,6 +163,14 @@ namespace Backend.MarketDataRequest
fundamentalsTcs.TrySetException(
new InvalidOperationException($"IB error for fundamentals reqId={id}: {errorCode} - {errorMsg}"));
return;
}
// Symbol lookup
if (id > 0 && _symbolLookupRequests.TryRemove(id, out var symbolLookupTcs))
{
symbolLookupTcs.TrySetException(
new InvalidOperationException($"IB error for symbol lookup reqId={id}: {errorCode} - {errorMsg}"));
}
}
@ -272,6 +283,51 @@ namespace Backend.MarketDataRequest
}
}
public ContractDescription[] RequestMatchingSymbolsBlocking(SymbolLookupRequest request, TimeSpan timeout)
{
ThrowIfDisposed();
request.Validate();
if (!Client.IsConnected())
throw new InvalidOperationException("IB Gateway is not connected.");
if (!_apiReady)
throw new InvalidOperationException("IB API is not ready yet.");
if (_symbolLookupRequests.ContainsKey(request.RequestId))
throw new InvalidOperationException($"Symbol lookup RequestId {request.RequestId} is already in use.");
var tcs = new TaskCompletionSource<ContractDescription[]>(TaskCreationOptions.RunContinuationsAsynchronously);
if (!_symbolLookupRequests.TryAdd(request.RequestId, tcs))
throw new InvalidOperationException($"Unable to register symbol lookup request {request.RequestId}.");
try
{
Console.WriteLine($"Requesting matching symbols: Pattern={request.Pattern}");
Client.reqMatchingSymbols(request.RequestId, request.Pattern);
if (tcs.Task.Wait(timeout))
return tcs.Task.GetAwaiter().GetResult();
_symbolLookupRequests.TryRemove(request.RequestId, out _);
throw new TimeoutException($"Timed out waiting for symbol lookup reqId={request.RequestId}.");
}
catch
{
_symbolLookupRequests.TryRemove(request.RequestId, out _);
throw;
}
}
public override void symbolSamples(int reqId, ContractDescription[] contractDescriptions)
{
Console.WriteLine($"[symbolSamples] reqId={reqId}, count={contractDescriptions.Length}");
if (_symbolLookupRequests.TryRemove(reqId, out var tcs))
tcs.TrySetResult(contractDescriptions);
}
public void Dispose()
{
if (_disposed)

View File

@ -0,0 +1,17 @@
namespace Backend.MarketDataRequest
{
public sealed class SymbolLookupRequest
{
public int RequestId { get; init; }
public string Pattern { get; init; } = string.Empty;
public void Validate()
{
if (RequestId <= 0)
throw new ArgumentException("RequestId must be greater than 0.");
if (string.IsNullOrWhiteSpace(Pattern))
throw new ArgumentException("Pattern is required.");
}
}
}

View File

@ -166,6 +166,39 @@ namespace Backend.ServiceHander
.ToList();
}
public (bool Found, string Message) CheckSymbol(string symbol)
{
var request = new SymbolLookupRequest
{
RequestId = GetNextRequestId(),
Pattern = symbol
};
ContractDescription[] results = Gateway.RequestMatchingSymbolsBlocking(request, TimeSpan.FromSeconds(10));
// Find exact symbol matches (IBKR pattern search may return partial matches)
var exactMatches = results
.Where(c => string.Equals(c.Contract.Symbol, symbol, StringComparison.OrdinalIgnoreCase))
.ToList();
bool isStockOrEtf = exactMatches.Any(c =>
string.Equals(c.Contract.SecType, "STK", StringComparison.OrdinalIgnoreCase) ||
string.Equals(c.Contract.SecType, "ETF", StringComparison.OrdinalIgnoreCase));
if (!isStockOrEtf)
return (false, "This symbol does not belong to any stock or ETF, please try again.");
bool isNasdaq = exactMatches.Any(c =>
(string.Equals(c.Contract.SecType, "STK", StringComparison.OrdinalIgnoreCase) ||
string.Equals(c.Contract.SecType, "ETF", StringComparison.OrdinalIgnoreCase)) &&
c.Contract.PrimaryExch.Contains("NASDAQ", StringComparison.OrdinalIgnoreCase));
if (!isNasdaq)
return (false, "Unfortunately, this symbol is currently unsupported. We only support NASDAQ listed stocks.");
return (true, "Symbol found.");
}
private static string NormalizeDuration(string duration)
{
return duration.Trim().Replace("+", " ");