SoftTraderBackend/ServiceHandler/MarketDataHander.cs

284 lines
7.2 KiB
C#

using Backend.DatabaseHandler;
using Backend.MarketDataRequest;
using Backend.Interface;
using IBApi;
using System.Globalization;
namespace Backend.ServiceHander
{
public sealed class MarketDataService
{
private readonly IBGatewayControl Gateway;
private readonly IHistoricalMarketDataCache Cache;
private int NextRequestId = 1;
private readonly object Lock = new();
public MarketDataService(IHistoricalMarketDataCache cache)
{
Cache = cache;
Gateway = new IBGatewayControl();
try
{
Gateway.Start("127.0.0.1", 4002, 1);
}
catch
{
Console.WriteLine("Invalid connection to IB Gateway. Please ensure gateway is running and connected.");
Environment.Exit(1);
}
}
private int GetNextRequestId()
{
lock (Lock)
{
return NextRequestId++;
}
}
public LiveEquitySnapshot GetLiveQuote(string symbol)
{
var request = new LiveEquityRequest
{
RequestId = GetNextRequestId(),
Symbol = symbol,
SecType = "STK",
Exchange = "SMART",
PrimaryExchange = "NASDAQ",
Currency = "USD",
RegulatorySnapshot = false
};
return Gateway.RequestLiveEquityBlocking(request, TimeSpan.FromSeconds(10));
}
public async Task<List<object>> GetHistoricalAsync(
string symbol,
string endDateTime,
string duration,
HistoricalBarSize barSize,
HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
bool useRth = true)
{
var bars = await GetHistoricalBarsAsync(
symbol,
endDateTime,
duration,
barSize,
whatToShow,
useRth);
return bars.Select(b => new
{
timestamp = ToIsoUtcString(b.TimestampUtc),
open = b.Open,
high = b.High,
low = b.Low,
close = b.Close,
volume = b.Volume
}).Cast<object>().ToList();
}
public async Task<List<object>> GetHistoricalLineAsync(
string symbol,
string endDateTime,
string duration,
HistoricalBarSize barSize,
HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
bool useRth = true)
{
var bars = await GetHistoricalBarsAsync(
symbol,
endDateTime,
duration,
barSize,
whatToShow,
useRth);
return bars.Select(b => new
{
timestamp = ToIsoUtcString(b.TimestampUtc),
price = b.Close,
volume = b.Volume
}).Cast<object>().ToList();
}
private async Task<List<HistoricalBarRecord>> GetHistoricalBarsAsync(
string symbol,
string endDateTimeIsoUtc,
string duration,
HistoricalBarSize barSize,
HistoricalWhatToShow whatToShow,
bool useRth)
{
var window = HistoricalRequestParser.BuildWindow(endDateTimeIsoUtc, duration, barSize);
var cachedBars = await Cache.GetBarsAsync(
symbol,
barSize,
whatToShow,
useRth,
window.StartUtc,
window.EndUtc);
if (cachedBars.Count >= window.ExpectedBarCountMin)
{
return cachedBars;
}
var request = new HistoricalEquityRequest
{
RequestId = GetNextRequestId(),
Symbol = symbol,
EndDateTime = ToIbHistoricalEndDateTime(window.EndUtc),
Duration = NormalizeDuration(duration),
BarSize = barSize,
WhatToShow = whatToShow,
UseRth = useRth,
KeepUpToDate = false,
FormatDate = 1
};
List<Bar> ibBars = Gateway.RequestHistoricalBlocking(request, TimeSpan.FromSeconds(20));
var mappedBars = ibBars.Select(b => new HistoricalBarRecord
{
Symbol = symbol,
BarSize = barSize.ToIbString(),
WhatToShow = whatToShow.ToIbString(),
UseRth = useRth,
TimestampUtc = ParseIbBarTimeToUtc(b.Time),
Open = b.Open,
High = b.High,
Low = b.Low,
Close = b.Close,
Volume = (long)b.Volume
})
.OrderBy(b => b.TimestampUtc)
.ToList();
await Cache.UpsertBarsAsync(mappedBars);
return mappedBars
.Where(b => b.TimestampUtc >= window.StartUtc && b.TimestampUtc <= window.EndUtc)
.OrderBy(b => b.TimestampUtc)
.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("+", " ");
}
private static string ToIsoUtcString(DateTime utc)
{
return utc.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
}
private static string ToIbHistoricalEndDateTime(DateTime utc)
{
var eastern = GetEasternTimeZone();
var easternTime = TimeZoneInfo.ConvertTimeFromUtc(utc.ToUniversalTime(), eastern);
return easternTime.ToString("yyyyMMdd HH:mm:ss", CultureInfo.InvariantCulture) + " US/Eastern";
}
private static DateTime ParseIbBarTimeToUtc(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
throw new FormatException("IB bar time is empty.");
raw = raw.Trim();
const string easternSuffix = " US/Eastern";
if (raw.EndsWith(easternSuffix, StringComparison.Ordinal))
{
string localPart = raw[..^easternSuffix.Length];
if (!DateTime.TryParseExact(
localPart,
"yyyyMMdd HH:mm:ss",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var easternLocal))
{
throw new FormatException($"Unable to parse IB datetime portion: {localPart}");
}
return TimeZoneInfo.ConvertTimeToUtc(easternLocal, GetEasternTimeZone());
}
if (DateTime.TryParseExact(
raw,
"yyyyMMdd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var dayOnly))
{
// Daily bars: treat as midnight UTC for storage consistency.
return DateTime.SpecifyKind(dayOnly, DateTimeKind.Utc);
}
if (DateTimeOffset.TryParseExact(
raw,
new[] { "yyyy-MM-ddTHH:mm:ss'Z'", "O" },
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var iso))
{
return iso.UtcDateTime;
}
throw new FormatException($"Unable to parse IB bar time: {raw}");
}
private static TimeZoneInfo GetEasternTimeZone()
{
return TimeZoneInfo.FindSystemTimeZoneById(
OperatingSystem.IsWindows() ? "Eastern Standard Time" : "America/New_York");
}
public void Dispose()
{
Gateway.Dispose();
}
public void Disconnect()
{
Gateway.Disconnect();
}
}
}