diff --git a/Backend.csproj b/Backend.csproj
new file mode 100644
index 0000000..4d2f476
--- /dev/null
+++ b/Backend.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/Functions.cs b/Functions.cs
new file mode 100644
index 0000000..e69de29
diff --git a/MarketDataRequest/HistoricalEquityRequest.cs b/MarketDataRequest/HistoricalEquityRequest.cs
new file mode 100644
index 0000000..aa51b72
--- /dev/null
+++ b/MarketDataRequest/HistoricalEquityRequest.cs
@@ -0,0 +1,77 @@
+namespace Backend.MarketDataRequest
+{
+ public enum HistoricalBarSize
+ {
+ OneMin,
+ FiveMins,
+ FifteenMins,
+ OneHour,
+ OneDay
+ }
+
+ public enum HistoricalWhatToShow
+ {
+ Trades,
+ Bid,
+ Ask,
+ Midpoint
+ }
+
+ public sealed class HistoricalEquityRequest
+ {
+ public int RequestId { get; init; }
+ public string Symbol { get; init; } = string.Empty;
+
+ // Example: "" or "20260311 11:00:00 US/Eastern"
+ public string EndDateTime { get; init; } = string.Empty;
+
+ // Examples: "1 H", "1 D", "5 D", "1 M"
+ public string Duration { get; init; } = "1 D";
+
+ public HistoricalBarSize BarSize { get; init; } = HistoricalBarSize.OneMin;
+ public HistoricalWhatToShow WhatToShow { get; init; } = HistoricalWhatToShow.Trades;
+
+ public bool UseRth { get; init; } = true;
+ public bool KeepUpToDate { get; init; } = false;
+
+ // IBKR formatDate=1 returns human-readable dates for intraday bars.
+ public int FormatDate { get; init; } = 1;
+
+ public void Validate()
+ {
+ if (RequestId <= 0)
+ throw new ArgumentException("RequestId must be greater than 0.");
+
+ if (string.IsNullOrWhiteSpace(Symbol))
+ throw new ArgumentException("Symbol is required.");
+
+ if (string.IsNullOrWhiteSpace(Duration))
+ throw new ArgumentException("Duration is required.");
+
+ if (string.IsNullOrWhiteSpace(EndDateTime))
+ throw new ArgumentException("EndDateTime is required for this example.");
+ }
+ }
+
+ public static class HistoricalRequestMappings
+ {
+ public static string ToIbString(this HistoricalBarSize barSize) => barSize switch
+ {
+ HistoricalBarSize.OneMin => "1 min",
+ HistoricalBarSize.FiveMins => "5 mins",
+ HistoricalBarSize.FifteenMins => "15 mins",
+ HistoricalBarSize.OneHour => "1 hour",
+ HistoricalBarSize.OneDay => "1 day",
+ _ => throw new ArgumentOutOfRangeException(nameof(barSize))
+ };
+
+ public static string ToIbString(this HistoricalWhatToShow whatToShow) => whatToShow switch
+ {
+ HistoricalWhatToShow.Trades => "TRADES",
+ HistoricalWhatToShow.Bid => "BID",
+ HistoricalWhatToShow.Ask => "ASK",
+ HistoricalWhatToShow.Midpoint => "MIDPOINT",
+ _ => throw new ArgumentOutOfRangeException(nameof(whatToShow))
+ };
+ }
+}
\ No newline at end of file
diff --git a/MarketDataRequest/IBGatewayControl.cs b/MarketDataRequest/IBGatewayControl.cs
new file mode 100644
index 0000000..efd7065
--- /dev/null
+++ b/MarketDataRequest/IBGatewayControl.cs
@@ -0,0 +1,257 @@
+using IBApi;
+using System.Collections.Concurrent;
+
+namespace Backend.MarketDataRequest
+{
+ public sealed class IBGatewayControl : DefaultEWrapper, IDisposable
+ {
+ private readonly EReaderMonitorSignal _signal = new();
+ public EClientSocket Client { get; }
+ private EReader? _reader;
+ private Thread? _readerThread;
+ private volatile bool _disposed;
+ private volatile bool _readerRunning;
+ private volatile bool _apiReady;
+
+ public bool IsConnected => Client.IsConnected();
+ public bool IsApiReady => _apiReady;
+
+ private readonly ConcurrentDictionary> _barsByRequest = new();
+ private readonly ConcurrentDictionary>> _historyRequests = new();
+
+ public IBGatewayControl()
+ {
+ Client = new EClientSocket(this, _signal);
+ }
+
+ public void Start(string host = "127.0.0.1", int port = 4002, int clientId = 1)
+ {
+ ThrowIfDisposed();
+
+ if (Client.IsConnected())
+ return;
+
+ Console.WriteLine($"Connecting to IB Gateway at {host}:{port} with clientId={clientId}...");
+ Client.eConnect(host, port, clientId);
+
+ if (!Client.IsConnected())
+ throw new InvalidOperationException("Failed to connect to IB Gateway.");
+
+ _reader = new EReader(Client, _signal);
+ _reader.Start();
+
+ _readerRunning = true;
+ _readerThread = new Thread(ReadLoop)
+ {
+ IsBackground = true,
+ Name = "IBGatewayControl.EReader"
+ };
+ _readerThread.Start();
+
+ Console.WriteLine("IB Gateway socket connected.");
+ }
+
+ public void Disconnect()
+ {
+ if (_disposed)
+ return;
+
+ _readerRunning = false;
+ _apiReady = false;
+
+ if (Client.IsConnected())
+ {
+ Console.WriteLine("Disconnecting from IB Gateway...");
+ Client.eDisconnect();
+ }
+ }
+
+ private void ReadLoop()
+ {
+ try
+ {
+ while (_readerRunning && Client.IsConnected())
+ {
+ _signal.waitForSignal();
+ _reader?.processMsgs();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[IBGatewayControl reader error] {ex}");
+ }
+ }
+
+ public override void connectAck()
+ {
+ Console.WriteLine("[connectAck]");
+ }
+
+ public override void nextValidId(int orderId)
+ {
+ _apiReady = true;
+ Console.WriteLine($"[nextValidId] {orderId}");
+ }
+
+ public override void managedAccounts(string accountsList)
+ {
+ Console.WriteLine($"[managedAccounts] {accountsList}");
+ }
+
+ public override void connectionClosed()
+ {
+ _apiReady = false;
+ Console.WriteLine("[connectionClosed]");
+ }
+
+ public override void error(Exception e)
+ {
+ Console.WriteLine($"[error exception] {e}");
+ }
+
+ public override void error(string str)
+ {
+ Console.WriteLine($"[error string] {str}");
+ }
+
+ public override void error(int id, long errorCode, int errorTime, string errorMsg, string advancedOrderRejectJson)
+ {
+ if (errorCode == 2104 || errorCode == 2106 || errorCode == 2158)
+ return;
+
+ Console.WriteLine($"[error] id={id}, code={errorCode}, time={errorTime}, msg={errorMsg}");
+
+ if (id > 0 && _historyRequests.TryRemove(id, out var tcs))
+ {
+ _barsByRequest.TryRemove(id, out _);
+ tcs.TrySetException(
+ new InvalidOperationException($"IB error for reqId={id}: {errorCode} - {errorMsg}"));
+ }
+ }
+
+ public void RequestHistorical(HistoricalEquityRequest request)
+ {
+ 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.");
+
+ var contract = new Contract
+ {
+ Symbol = request.Symbol,
+ SecType = "STK",
+ Exchange = "SMART",
+ PrimaryExch = "NASDAQ",
+ Currency = "USD"
+ };
+
+ _barsByRequest[request.RequestId] = new List();
+
+ Console.WriteLine(
+ $"Requesting historical data: Symbol={request.Symbol}, End={request.EndDateTime}, Duration={request.Duration}, BarSize={request.BarSize.ToIbString()}");
+
+ Client.reqHistoricalData(
+ request.RequestId,
+ contract,
+ request.EndDateTime,
+ request.Duration,
+ request.BarSize.ToIbString(),
+ request.WhatToShow.ToIbString(),
+ request.UseRth ? 1 : 0,
+ request.FormatDate,
+ request.KeepUpToDate,
+ null
+ );
+ }
+
+ public List RequestHistoricalBlocking(HistoricalEquityRequest request, TimeSpan timeout)
+ {
+ ThrowIfDisposed();
+ request.Validate();
+
+ if (_historyRequests.ContainsKey(request.RequestId))
+ throw new InvalidOperationException($"RequestId {request.RequestId} is already in use.");
+
+ var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ if (!_historyRequests.TryAdd(request.RequestId, tcs))
+ throw new InvalidOperationException($"Unable to register RequestId {request.RequestId}.");
+
+ try
+ {
+ RequestHistorical(request);
+
+ if (tcs.Task.Wait(timeout))
+ return tcs.Task.GetAwaiter().GetResult();
+
+ _historyRequests.TryRemove(request.RequestId, out _);
+ _barsByRequest.TryRemove(request.RequestId, out _);
+ throw new TimeoutException($"Timed out waiting for historical data for reqId={request.RequestId}.");
+ }
+ catch
+ {
+ _historyRequests.TryRemove(request.RequestId, out _);
+ _barsByRequest.TryRemove(request.RequestId, out _);
+ throw;
+ }
+ }
+
+ public override void historicalData(int reqId, Bar bar)
+ {
+ if (_barsByRequest.TryGetValue(reqId, out var bars))
+ {
+ lock (bars)
+ {
+ bars.Add(bar);
+ }
+ }
+
+ Console.WriteLine(
+ $"[historicalData] reqId={reqId}, Date={bar.Time}, Open={bar.Open}, High={bar.High}, Low={bar.Low}, Close={bar.Close}, Volume={bar.Volume}");
+ }
+
+ public override void historicalDataEnd(int reqId, string start, string end)
+ {
+ Console.WriteLine($"[historicalDataEnd] reqId={reqId}, start={start}, end={end}");
+
+ if (_barsByRequest.TryRemove(reqId, out var bars))
+ {
+ Console.WriteLine();
+ Console.WriteLine("=== Final Printed Bars ===");
+ lock (bars)
+ {
+ foreach (var bar in bars)
+ {
+ Console.WriteLine(
+ $"{bar.Time} | O:{bar.Open} H:{bar.High} L:{bar.Low} C:{bar.Close} V:{bar.Volume}");
+ }
+ }
+
+ if (_historyRequests.TryRemove(reqId, out var tcs))
+ {
+ tcs.TrySetResult(bars);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ Disconnect();
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(IBGatewayControl));
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarketDataRequest/LiveEquityRequest.cs b/MarketDataRequest/LiveEquityRequest.cs
new file mode 100644
index 0000000..96b1015
--- /dev/null
+++ b/MarketDataRequest/LiveEquityRequest.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using IBApi;
+
+namespace Backend.MarketDataRequest
+{
+ public enum LiveMarketDataType
+ {
+ RealTime = 1,
+ Frozen = 2,
+ Delayed = 3,
+ DelayedFrozen = 4
+ }
+
+ public sealed class LiveEquityRequest
+ {
+ public int RequestId { get; set; }
+ public string Symbol { get; set; } = string.Empty;
+ public string Exchange { get; set; } = "SMART";
+ public string PrimaryExchange { get; set; } = "NASDAQ";
+ public string Currency { get; set; } = "USD";
+
+ ///
+ /// Generic ticks string, e.g. "233,236"
+ /// Leave empty for standard top-of-book.
+ ///
+ public string GenericTickList { get; set; } = string.Empty;
+
+ public bool Snapshot { get; set; } = false;
+ public bool RegulatorySnapshot { get; set; } = false;
+ public LiveMarketDataType MarketDataType { get; set; } = LiveMarketDataType.RealTime;
+
+ public void Send(IBGatewayControl gateway)
+ {
+ if (gateway == null)
+ {
+ throw new ArgumentNullException(nameof(gateway));
+ }
+
+ if (!gateway.IsConnected)
+ {
+ throw new InvalidOperationException("IB Gateway is not connected.");
+ }
+
+ if (string.IsNullOrWhiteSpace(Symbol))
+ {
+ throw new ArgumentException("Symbol cannot be empty.");
+ }
+
+ gateway.Client.reqMarketDataType((int)MarketDataType);
+
+ var contract = BuildContract();
+
+ gateway.Client.reqMktData(
+ RequestId,
+ contract,
+ GenericTickList,
+ Snapshot,
+ RegulatorySnapshot,
+ new List()
+ );
+ }
+
+ public void Cancel(IBGatewayControl gateway)
+ {
+ if (gateway == null)
+ {
+ throw new ArgumentNullException(nameof(gateway));
+ }
+
+ if (!gateway.IsConnected)
+ {
+ throw new InvalidOperationException("IB Gateway is not connected.");
+ }
+
+ gateway.Client.cancelMktData(RequestId);
+ }
+
+ private Contract BuildContract()
+ {
+ return new Contract
+ {
+ Symbol = Symbol,
+ SecType = "STK",
+ Exchange = Exchange,
+ PrimaryExch = PrimaryExchange,
+ Currency = Currency
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..e55e991
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,46 @@
+using Backend.MarketDataRequest;
+using IBApi;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Backend
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var Gateway = new IBGatewayControl();
+ Gateway.Start("127.0.0.1", 4002, 1);
+
+ while (!Gateway.IsApiReady)
+ {
+ Thread.Sleep(50);
+ }
+
+ var request = new HistoricalEquityRequest
+ {
+ RequestId = 1,
+ Symbol = "NVDA",
+ EndDateTime = "20260311 11:00:00 US/Eastern",
+ Duration = "3600 S",
+ BarSize = HistoricalBarSize.FiveMins,
+ WhatToShow = HistoricalWhatToShow.Trades,
+ UseRth = true,
+ KeepUpToDate = false,
+ FormatDate = 1
+ };
+
+ List bars = Gateway.RequestHistoricalBlocking(
+ request,
+ TimeSpan.FromSeconds(15));
+
+ Console.WriteLine();
+ Console.WriteLine($"Received {bars.Count} bars. Continue program here.");
+
+ // do next step here, for example save to database
+
+ Gateway.Disconnect();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
new file mode 100644
index 0000000..3cbd1d4
--- /dev/null
+++ b/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5246",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7039;http://localhost:5246",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/appsettings.Development.json b/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}