diff --git a/Controller/MarketDataController.cs b/Controller/MarketDataController.cs index 6d2568a..608bc87 100644 --- a/Controller/MarketDataController.cs +++ b/Controller/MarketDataController.cs @@ -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) { diff --git a/MarketDataRequest/IBGatewayControl.cs b/MarketDataRequest/IBGatewayControl.cs index a9eb51e..7e2f241 100644 --- a/MarketDataRequest/IBGatewayControl.cs +++ b/MarketDataRequest/IBGatewayControl.cs @@ -29,6 +29,9 @@ namespace Backend.MarketDataRequest private readonly ConcurrentDictionary _fundamentalsSnapshotsByRequest = new(); private readonly ConcurrentDictionary> _fundamentalsRequests = new(); + // Symbol lookup data + private readonly ConcurrentDictionary> _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(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) diff --git a/MarketDataRequest/SymbolLookupRequest.cs b/MarketDataRequest/SymbolLookupRequest.cs new file mode 100644 index 0000000..b2bfbf5 --- /dev/null +++ b/MarketDataRequest/SymbolLookupRequest.cs @@ -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."); + } + } +} diff --git a/ServiceHandler/MarketDataHander.cs b/ServiceHandler/MarketDataHander.cs index d7c865e..3209808 100644 --- a/ServiceHandler/MarketDataHander.cs +++ b/ServiceHandler/MarketDataHander.cs @@ -166,7 +166,40 @@ namespace Backend.ServiceHander .ToList(); } - private static string NormalizeDuration(string duration) + 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("+", " "); }