Fixed database price history caching issue.
This commit is contained in:
parent
f9ba5369bd
commit
df759d0ead
@ -15,6 +15,20 @@ namespace Backend.Controller
|
|||||||
_marketDataService = marketDataService;
|
_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}")]
|
[HttpGet("live/{symbol}")]
|
||||||
public IActionResult GetLive(string symbol)
|
public IActionResult GetLive(string symbol)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,6 +29,9 @@ namespace Backend.MarketDataRequest
|
|||||||
private readonly ConcurrentDictionary<int, FundamentalsEquitySnapshot> _fundamentalsSnapshotsByRequest = new();
|
private readonly ConcurrentDictionary<int, FundamentalsEquitySnapshot> _fundamentalsSnapshotsByRequest = new();
|
||||||
private readonly ConcurrentDictionary<int, TaskCompletionSource<FundamentalsEquitySnapshot>> _fundamentalsRequests = new();
|
private readonly ConcurrentDictionary<int, TaskCompletionSource<FundamentalsEquitySnapshot>> _fundamentalsRequests = new();
|
||||||
|
|
||||||
|
// Symbol lookup data
|
||||||
|
private readonly ConcurrentDictionary<int, TaskCompletionSource<ContractDescription[]>> _symbolLookupRequests = new();
|
||||||
|
|
||||||
public IBGatewayControl()
|
public IBGatewayControl()
|
||||||
{
|
{
|
||||||
Client = new EClientSocket(this, _signal);
|
Client = new EClientSocket(this, _signal);
|
||||||
@ -160,6 +163,14 @@ namespace Backend.MarketDataRequest
|
|||||||
|
|
||||||
fundamentalsTcs.TrySetException(
|
fundamentalsTcs.TrySetException(
|
||||||
new InvalidOperationException($"IB error for fundamentals reqId={id}: {errorCode} - {errorMsg}"));
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|||||||
17
MarketDataRequest/SymbolLookupRequest.cs
Normal file
17
MarketDataRequest/SymbolLookupRequest.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -166,6 +166,39 @@ namespace Backend.ServiceHander
|
|||||||
.ToList();
|
.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)
|
private static string NormalizeDuration(string duration)
|
||||||
{
|
{
|
||||||
return duration.Trim().Replace("+", " ");
|
return duration.Trim().Replace("+", " ");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user