Working synchronous historical data request.
This commit is contained in:
parent
a3604f665e
commit
d1328a88d9
14
Backend.csproj
Normal file
14
Backend.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../twsapi/CSharpAPI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
0
Functions.cs
Normal file
0
Functions.cs
Normal file
77
MarketDataRequest/HistoricalEquityRequest.cs
Normal file
77
MarketDataRequest/HistoricalEquityRequest.cs
Normal file
@ -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))
|
||||
};
|
||||
}
|
||||
}
|
||||
257
MarketDataRequest/IBGatewayControl.cs
Normal file
257
MarketDataRequest/IBGatewayControl.cs
Normal file
@ -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<int, List<Bar>> _barsByRequest = new();
|
||||
private readonly ConcurrentDictionary<int, TaskCompletionSource<List<Bar>>> _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<Bar>();
|
||||
|
||||
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<Bar> 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<List<Bar>>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
91
MarketDataRequest/LiveEquityRequest.cs
Normal file
91
MarketDataRequest/LiveEquityRequest.cs
Normal file
@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// Generic ticks string, e.g. "233,236"
|
||||
/// Leave empty for standard top-of-book.
|
||||
/// </summary>
|
||||
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<TagValue>()
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Program.cs
Normal file
46
Program.cs
Normal file
@ -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<Bar> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Properties/launchSettings.json
Normal file
23
Properties/launchSettings.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
appsettings.json
Normal file
9
appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user