Added support for live market data.
This commit is contained in:
parent
d1328a88d9
commit
f5e2d70f57
36
MarketDataRequest/FundamentalsEquityRequest.cs
Normal file
36
MarketDataRequest/FundamentalsEquityRequest.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Backend.MarketDataRequest
|
||||||
|
{
|
||||||
|
public sealed class FundamentalsEquityRequest
|
||||||
|
{
|
||||||
|
public int RequestId { get; init; }
|
||||||
|
public string Symbol { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string SecType { get; init; } = "STK";
|
||||||
|
public string Exchange { get; init; } = "SMART";
|
||||||
|
public string PrimaryExchange { get; init; } = "NASDAQ";
|
||||||
|
public string Currency { get; init; } = "USD";
|
||||||
|
|
||||||
|
public bool RegulatorySnapshot { get; init; } = false;
|
||||||
|
|
||||||
|
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(SecType))
|
||||||
|
throw new ArgumentException("SecType is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Exchange))
|
||||||
|
throw new ArgumentException("Exchange is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(PrimaryExchange))
|
||||||
|
throw new ArgumentException("PrimaryExchange is required.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Currency))
|
||||||
|
throw new ArgumentException("Currency is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
MarketDataRequest/FundamentalsEquitySnapshot.cs
Normal file
25
MarketDataRequest/FundamentalsEquitySnapshot.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace Backend.MarketDataRequest
|
||||||
|
{
|
||||||
|
public sealed class FundamentalsEquitySnapshot
|
||||||
|
{
|
||||||
|
public string Symbol { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public double? MarketCap { get; set; }
|
||||||
|
public double? PERatio { get; set; }
|
||||||
|
public double? DividendYield { get; set; }
|
||||||
|
|
||||||
|
public bool IsComplete =>
|
||||||
|
MarketCap.HasValue &&
|
||||||
|
PERatio.HasValue &&
|
||||||
|
DividendYield.HasValue;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"Symbol={Symbol}, " +
|
||||||
|
$"MarketCap={MarketCap}, " +
|
||||||
|
$"PERatio={PERatio}, " +
|
||||||
|
$"DividendYield={DividendYield}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,14 +55,14 @@ namespace Backend.MarketDataRequest
|
|||||||
|
|
||||||
public static class HistoricalRequestMappings
|
public static class HistoricalRequestMappings
|
||||||
{
|
{
|
||||||
public static string ToIbString(this HistoricalBarSize barSize) => barSize switch
|
public static string ToIbString(this HistoricalBarSize BarSize) => BarSize switch
|
||||||
{
|
{
|
||||||
HistoricalBarSize.OneMin => "1 min",
|
HistoricalBarSize.OneMin => "1 min",
|
||||||
HistoricalBarSize.FiveMins => "5 mins",
|
HistoricalBarSize.FiveMins => "5 mins",
|
||||||
HistoricalBarSize.FifteenMins => "15 mins",
|
HistoricalBarSize.FifteenMins => "15 mins",
|
||||||
HistoricalBarSize.OneHour => "1 hour",
|
HistoricalBarSize.OneHour => "1 hour",
|
||||||
HistoricalBarSize.OneDay => "1 day",
|
HistoricalBarSize.OneDay => "1 day",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(barSize))
|
_ => throw new ArgumentOutOfRangeException(nameof(BarSize))
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ToIbString(this HistoricalWhatToShow whatToShow) => whatToShow switch
|
public static string ToIbString(this HistoricalWhatToShow whatToShow) => whatToShow switch
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using IBApi;
|
using IBApi;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Backend.MarketDataRequest
|
namespace Backend.MarketDataRequest
|
||||||
{
|
{
|
||||||
@ -16,9 +17,18 @@ namespace Backend.MarketDataRequest
|
|||||||
public bool IsConnected => Client.IsConnected();
|
public bool IsConnected => Client.IsConnected();
|
||||||
public bool IsApiReady => _apiReady;
|
public bool IsApiReady => _apiReady;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<int, List<Bar>> _barsByRequest = new();
|
// Historical data
|
||||||
|
private readonly ConcurrentDictionary<int, List<Bar>> _BarsByRequest = new();
|
||||||
private readonly ConcurrentDictionary<int, TaskCompletionSource<List<Bar>>> _historyRequests = new();
|
private readonly ConcurrentDictionary<int, TaskCompletionSource<List<Bar>>> _historyRequests = new();
|
||||||
|
|
||||||
|
// Equity data
|
||||||
|
private readonly ConcurrentDictionary<int, LiveEquitySnapshot> _liveSnapshotsByRequest = new();
|
||||||
|
private readonly ConcurrentDictionary<int, TaskCompletionSource<LiveEquitySnapshot>> _liveRequests = new();
|
||||||
|
|
||||||
|
//Fundamentals data
|
||||||
|
private readonly ConcurrentDictionary<int, FundamentalsEquitySnapshot> _fundamentalsSnapshotsByRequest = new();
|
||||||
|
private readonly ConcurrentDictionary<int, TaskCompletionSource<FundamentalsEquitySnapshot>> _fundamentalsRequests = new();
|
||||||
|
|
||||||
public IBGatewayControl()
|
public IBGatewayControl()
|
||||||
{
|
{
|
||||||
Client = new EClientSocket(this, _signal);
|
Client = new EClientSocket(this, _signal);
|
||||||
@ -121,11 +131,23 @@ namespace Backend.MarketDataRequest
|
|||||||
|
|
||||||
Console.WriteLine($"[error] id={id}, code={errorCode}, time={errorTime}, msg={errorMsg}");
|
Console.WriteLine($"[error] id={id}, code={errorCode}, time={errorTime}, msg={errorMsg}");
|
||||||
|
|
||||||
if (id > 0 && _historyRequests.TryRemove(id, out var tcs))
|
if (id > 0 && _liveRequests.TryRemove(id, out var liveTcs))
|
||||||
{
|
{
|
||||||
_barsByRequest.TryRemove(id, out _);
|
Client.cancelMktData(id);
|
||||||
tcs.TrySetException(
|
_liveSnapshotsByRequest.TryRemove(id, out _);
|
||||||
new InvalidOperationException($"IB error for reqId={id}: {errorCode} - {errorMsg}"));
|
|
||||||
|
liveTcs.TrySetException(
|
||||||
|
new InvalidOperationException($"IB error for live reqId={id}: {errorCode} - {errorMsg}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id > 0 && _fundamentalsRequests.TryRemove(id, out var fundamentalsTcs))
|
||||||
|
{
|
||||||
|
Client.cancelMktData(id);
|
||||||
|
_fundamentalsSnapshotsByRequest.TryRemove(id, out _);
|
||||||
|
|
||||||
|
fundamentalsTcs.TrySetException(
|
||||||
|
new InvalidOperationException($"IB error for fundamentals reqId={id}: {errorCode} - {errorMsg}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +171,7 @@ namespace Backend.MarketDataRequest
|
|||||||
Currency = "USD"
|
Currency = "USD"
|
||||||
};
|
};
|
||||||
|
|
||||||
_barsByRequest[request.RequestId] = new List<Bar>();
|
_BarsByRequest[request.RequestId] = new List<Bar>();
|
||||||
|
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"Requesting historical data: Symbol={request.Symbol}, End={request.EndDateTime}, Duration={request.Duration}, BarSize={request.BarSize.ToIbString()}");
|
$"Requesting historical data: Symbol={request.Symbol}, End={request.EndDateTime}, Duration={request.Duration}, BarSize={request.BarSize.ToIbString()}");
|
||||||
@ -189,51 +211,51 @@ namespace Backend.MarketDataRequest
|
|||||||
return tcs.Task.GetAwaiter().GetResult();
|
return tcs.Task.GetAwaiter().GetResult();
|
||||||
|
|
||||||
_historyRequests.TryRemove(request.RequestId, out _);
|
_historyRequests.TryRemove(request.RequestId, out _);
|
||||||
_barsByRequest.TryRemove(request.RequestId, out _);
|
_BarsByRequest.TryRemove(request.RequestId, out _);
|
||||||
throw new TimeoutException($"Timed out waiting for historical data for reqId={request.RequestId}.");
|
throw new TimeoutException($"Timed out waiting for historical data for reqId={request.RequestId}.");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_historyRequests.TryRemove(request.RequestId, out _);
|
_historyRequests.TryRemove(request.RequestId, out _);
|
||||||
_barsByRequest.TryRemove(request.RequestId, out _);
|
_BarsByRequest.TryRemove(request.RequestId, out _);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void historicalData(int reqId, Bar bar)
|
public override void historicalData(int reqId, Bar Bar)
|
||||||
{
|
{
|
||||||
if (_barsByRequest.TryGetValue(reqId, out var bars))
|
if (_BarsByRequest.TryGetValue(reqId, out var Bars))
|
||||||
{
|
{
|
||||||
lock (bars)
|
lock (Bars)
|
||||||
{
|
{
|
||||||
bars.Add(bar);
|
Bars.Add(Bar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[historicalData] reqId={reqId}, Date={bar.Time}, Open={bar.Open}, High={bar.High}, Low={bar.Low}, Close={bar.Close}, Volume={bar.Volume}");
|
$"[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)
|
public override void historicalDataEnd(int reqId, string start, string end)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[historicalDataEnd] reqId={reqId}, start={start}, end={end}");
|
Console.WriteLine($"[historicalDataEnd] reqId={reqId}, start={start}, end={end}");
|
||||||
|
|
||||||
if (_barsByRequest.TryRemove(reqId, out var bars))
|
if (_BarsByRequest.TryRemove(reqId, out var Bars))
|
||||||
{
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("=== Final Printed Bars ===");
|
Console.WriteLine("=== Final Printed Bars ===");
|
||||||
lock (bars)
|
lock (Bars)
|
||||||
{
|
{
|
||||||
foreach (var bar in bars)
|
foreach (var Bar in Bars)
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"{bar.Time} | O:{bar.Open} H:{bar.High} L:{bar.Low} C:{bar.Close} V:{bar.Volume}");
|
$"{Bar.Time} | O:{Bar.Open} H:{Bar.High} L:{Bar.Low} C:{Bar.Close} V:{Bar.Volume}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_historyRequests.TryRemove(reqId, out var tcs))
|
if (_historyRequests.TryRemove(reqId, out var tcs))
|
||||||
{
|
{
|
||||||
tcs.TrySetResult(bars);
|
tcs.TrySetResult(Bars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,5 +275,304 @@ namespace Backend.MarketDataRequest
|
|||||||
if (_disposed)
|
if (_disposed)
|
||||||
throw new ObjectDisposedException(nameof(IBGatewayControl));
|
throw new ObjectDisposedException(nameof(IBGatewayControl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsValidNumber(double value)
|
||||||
|
{
|
||||||
|
return !double.IsNaN(value) &&
|
||||||
|
!double.IsInfinity(value) &&
|
||||||
|
value > -99999.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDouble(string raw, out double value)
|
||||||
|
{
|
||||||
|
return double.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ParseFundamentalRatios(string raw)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
string[] parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (string part in parts)
|
||||||
|
{
|
||||||
|
int idx = part.IndexOf('=');
|
||||||
|
if (idx <= 0 || idx >= part.Length - 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string key = part[..idx].Trim();
|
||||||
|
string value = part[(idx + 1)..].Trim();
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryFillFundamentals(FundamentalsEquitySnapshot snapshot, string raw)
|
||||||
|
{
|
||||||
|
var ratios = ParseFundamentalRatios(raw);
|
||||||
|
|
||||||
|
if (!snapshot.MarketCap.HasValue &&
|
||||||
|
ratios.TryGetValue("MKTCAP", out var marketCapRaw) &&
|
||||||
|
TryParseDouble(marketCapRaw, out var marketCap) &&
|
||||||
|
IsValidNumber(marketCap))
|
||||||
|
{
|
||||||
|
snapshot.MarketCap = marketCap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.PERatio.HasValue)
|
||||||
|
{
|
||||||
|
if (ratios.TryGetValue("PEEXCLXOR", out var peRaw) &&
|
||||||
|
TryParseDouble(peRaw, out var pe) &&
|
||||||
|
IsValidNumber(pe))
|
||||||
|
{
|
||||||
|
snapshot.PERatio = pe;
|
||||||
|
}
|
||||||
|
else if (ratios.TryGetValue("APENORM", out var peNormRaw) &&
|
||||||
|
TryParseDouble(peNormRaw, out var peNorm) &&
|
||||||
|
IsValidNumber(peNorm))
|
||||||
|
{
|
||||||
|
snapshot.PERatio = peNorm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.DividendYield.HasValue &&
|
||||||
|
ratios.TryGetValue("YIELD", out var yieldRaw) &&
|
||||||
|
TryParseDouble(yieldRaw, out var divYield) &&
|
||||||
|
IsValidNumber(divYield))
|
||||||
|
{
|
||||||
|
snapshot.DividendYield = divYield;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteLiveRequestIfReady(int reqId)
|
||||||
|
{
|
||||||
|
if (!_liveSnapshotsByRequest.TryGetValue(reqId, out var snapshot))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!snapshot.IsComplete)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Client.cancelMktData(reqId);
|
||||||
|
_liveSnapshotsByRequest.TryRemove(reqId, out _);
|
||||||
|
|
||||||
|
if (_liveRequests.TryRemove(reqId, out var tcs))
|
||||||
|
tcs.TrySetResult(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteFundamentalsRequestIfReady(int reqId)
|
||||||
|
{
|
||||||
|
if (!_fundamentalsSnapshotsByRequest.TryGetValue(reqId, out var snapshot))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!snapshot.IsComplete)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Client.cancelMktData(reqId);
|
||||||
|
_fundamentalsSnapshotsByRequest.TryRemove(reqId, out _);
|
||||||
|
|
||||||
|
if (_fundamentalsRequests.TryRemove(reqId, out var tcs))
|
||||||
|
tcs.TrySetResult(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveEquitySnapshot RequestLiveEquityBlocking(LiveEquityRequest 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 (_liveRequests.ContainsKey(request.RequestId))
|
||||||
|
throw new InvalidOperationException($"Live RequestId {request.RequestId} is already in use.");
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<LiveEquitySnapshot>(
|
||||||
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
if (!_liveRequests.TryAdd(request.RequestId, tcs))
|
||||||
|
throw new InvalidOperationException($"Unable to register live request {request.RequestId}.");
|
||||||
|
|
||||||
|
var snapshot = new LiveEquitySnapshot
|
||||||
|
{
|
||||||
|
Symbol = request.Symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
_liveSnapshotsByRequest[request.RequestId] = snapshot;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contract = new Contract
|
||||||
|
{
|
||||||
|
Symbol = request.Symbol,
|
||||||
|
SecType = request.SecType,
|
||||||
|
Exchange = request.Exchange,
|
||||||
|
PrimaryExch = request.PrimaryExchange,
|
||||||
|
Currency = request.Currency
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Requesting live quote data: Symbol={request.Symbol}, Ticks=Last/Volume/Close/Open");
|
||||||
|
|
||||||
|
Client.reqMktData(
|
||||||
|
request.RequestId,
|
||||||
|
contract,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
request.RegulatorySnapshot,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tcs.Task.Wait(timeout))
|
||||||
|
return tcs.Task.GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Client.cancelMktData(request.RequestId);
|
||||||
|
_liveRequests.TryRemove(request.RequestId, out _);
|
||||||
|
_liveSnapshotsByRequest.TryRemove(request.RequestId, out _);
|
||||||
|
|
||||||
|
throw new TimeoutException(
|
||||||
|
$"Timed out waiting for live quote data for reqId={request.RequestId}.");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Client.cancelMktData(request.RequestId);
|
||||||
|
_liveRequests.TryRemove(request.RequestId, out _);
|
||||||
|
_liveSnapshotsByRequest.TryRemove(request.RequestId, out _);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public FundamentalsEquitySnapshot RequestFundamentalsBlocking(FundamentalsEquityRequest 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 (_fundamentalsRequests.ContainsKey(request.RequestId))
|
||||||
|
throw new InvalidOperationException($"Fundamentals RequestId {request.RequestId} is already in use.");
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<FundamentalsEquitySnapshot>(
|
||||||
|
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
if (!_fundamentalsRequests.TryAdd(request.RequestId, tcs))
|
||||||
|
throw new InvalidOperationException($"Unable to register fundamentals request {request.RequestId}.");
|
||||||
|
|
||||||
|
var snapshot = new FundamentalsEquitySnapshot
|
||||||
|
{
|
||||||
|
Symbol = request.Symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
_fundamentalsSnapshotsByRequest[request.RequestId] = snapshot;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var contract = new Contract
|
||||||
|
{
|
||||||
|
Symbol = request.Symbol,
|
||||||
|
SecType = request.SecType,
|
||||||
|
Exchange = request.Exchange,
|
||||||
|
PrimaryExch = request.PrimaryExchange,
|
||||||
|
Currency = request.Currency
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"Requesting fundamentals data: Symbol={request.Symbol}, GenericTicks=258");
|
||||||
|
|
||||||
|
Client.reqMktData(
|
||||||
|
request.RequestId,
|
||||||
|
contract,
|
||||||
|
"258",
|
||||||
|
false,
|
||||||
|
request.RegulatorySnapshot,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tcs.Task.Wait(timeout))
|
||||||
|
return tcs.Task.GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Client.cancelMktData(request.RequestId);
|
||||||
|
_fundamentalsRequests.TryRemove(request.RequestId, out _);
|
||||||
|
_fundamentalsSnapshotsByRequest.TryRemove(request.RequestId, out _);
|
||||||
|
|
||||||
|
throw new TimeoutException(
|
||||||
|
$"Timed out waiting for fundamentals data for reqId={request.RequestId}.");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Client.cancelMktData(request.RequestId);
|
||||||
|
_fundamentalsRequests.TryRemove(request.RequestId, out _);
|
||||||
|
_fundamentalsSnapshotsByRequest.TryRemove(request.RequestId, out _);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void tickPrice(int tickerId, int field, double price, TickAttrib attribs)
|
||||||
|
{
|
||||||
|
if (_liveSnapshotsByRequest.TryGetValue(tickerId, out var liveSnapshot))
|
||||||
|
{
|
||||||
|
if (IsValidNumber(price))
|
||||||
|
{
|
||||||
|
switch (field)
|
||||||
|
{
|
||||||
|
case 4: // Last Price
|
||||||
|
liveSnapshot.LastPrice = price;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 9: // Close Price
|
||||||
|
liveSnapshot.PreviousClose = price;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 14: // Open Tick
|
||||||
|
liveSnapshot.Open = price;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompleteLiveRequestIfReady(tickerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[tickPrice] reqId={tickerId}, field={field}, price={price}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void tickSize(int tickerId, int field, decimal size)
|
||||||
|
{
|
||||||
|
if (_liveSnapshotsByRequest.TryGetValue(tickerId, out var liveSnapshot))
|
||||||
|
{
|
||||||
|
switch (field)
|
||||||
|
{
|
||||||
|
case 8: // Volume
|
||||||
|
liveSnapshot.Volume = (long)size;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompleteLiveRequestIfReady(tickerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[tickSize] reqId={tickerId}, field={field}, size={size}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void tickString(int tickerId, int field, string value)
|
||||||
|
{
|
||||||
|
if (_fundamentalsSnapshotsByRequest.TryGetValue(tickerId, out var fundamentalsSnapshot))
|
||||||
|
{
|
||||||
|
if (field == 47)
|
||||||
|
{
|
||||||
|
TryFillFundamentals(fundamentalsSnapshot, value);
|
||||||
|
CompleteFundamentalsRequestIfReady(tickerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[tickString] reqId={tickerId}, field={field}, value={value}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,91 +1,36 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using IBApi;
|
|
||||||
|
|
||||||
namespace Backend.MarketDataRequest
|
namespace Backend.MarketDataRequest
|
||||||
{
|
{
|
||||||
public enum LiveMarketDataType
|
|
||||||
{
|
|
||||||
RealTime = 1,
|
|
||||||
Frozen = 2,
|
|
||||||
Delayed = 3,
|
|
||||||
DelayedFrozen = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class LiveEquityRequest
|
public sealed class LiveEquityRequest
|
||||||
{
|
{
|
||||||
public int RequestId { get; set; }
|
public int RequestId { get; init; }
|
||||||
public string Symbol { get; set; } = string.Empty;
|
public string Symbol { get; init; } = string.Empty;
|
||||||
public string Exchange { get; set; } = "SMART";
|
|
||||||
public string PrimaryExchange { get; set; } = "NASDAQ";
|
|
||||||
public string Currency { get; set; } = "USD";
|
|
||||||
|
|
||||||
/// <summary>
|
public string SecType { get; init; } = "STK";
|
||||||
/// Generic ticks string, e.g. "233,236"
|
public string Exchange { get; init; } = "SMART";
|
||||||
/// Leave empty for standard top-of-book.
|
public string PrimaryExchange { get; init; } = "NASDAQ";
|
||||||
/// </summary>
|
public string Currency { get; init; } = "USD";
|
||||||
public string GenericTickList { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool Snapshot { get; set; } = false;
|
public bool RegulatorySnapshot { get; init; } = false;
|
||||||
public bool RegulatorySnapshot { get; set; } = false;
|
|
||||||
public LiveMarketDataType MarketDataType { get; set; } = LiveMarketDataType.RealTime;
|
|
||||||
|
|
||||||
public void Send(IBGatewayControl gateway)
|
public void Validate()
|
||||||
{
|
{
|
||||||
if (gateway == null)
|
if (RequestId <= 0)
|
||||||
{
|
throw new ArgumentException("RequestId must be greater than 0.");
|
||||||
throw new ArgumentNullException(nameof(gateway));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gateway.IsConnected)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("IB Gateway is not connected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Symbol))
|
if (string.IsNullOrWhiteSpace(Symbol))
|
||||||
{
|
throw new ArgumentException("Symbol is required.");
|
||||||
throw new ArgumentException("Symbol cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway.Client.reqMarketDataType((int)MarketDataType);
|
if (string.IsNullOrWhiteSpace(SecType))
|
||||||
|
throw new ArgumentException("SecType is required.");
|
||||||
|
|
||||||
var contract = BuildContract();
|
if (string.IsNullOrWhiteSpace(Exchange))
|
||||||
|
throw new ArgumentException("Exchange is required.");
|
||||||
|
|
||||||
gateway.Client.reqMktData(
|
if (string.IsNullOrWhiteSpace(PrimaryExchange))
|
||||||
RequestId,
|
throw new ArgumentException("PrimaryExchange is required.");
|
||||||
contract,
|
|
||||||
GenericTickList,
|
|
||||||
Snapshot,
|
|
||||||
RegulatorySnapshot,
|
|
||||||
new List<TagValue>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Cancel(IBGatewayControl gateway)
|
if (string.IsNullOrWhiteSpace(Currency))
|
||||||
{
|
throw new ArgumentException("Currency is required.");
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
MarketDataRequest/LiveEquitySnapshot.cs
Normal file
28
MarketDataRequest/LiveEquitySnapshot.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Backend.MarketDataRequest
|
||||||
|
{
|
||||||
|
public sealed class LiveEquitySnapshot
|
||||||
|
{
|
||||||
|
public string Symbol { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public double? LastPrice { get; set; }
|
||||||
|
public long? Volume { get; set; }
|
||||||
|
public double? PreviousClose { get; set; }
|
||||||
|
public double? Open { get; set; }
|
||||||
|
|
||||||
|
public bool IsComplete =>
|
||||||
|
LastPrice.HasValue &&
|
||||||
|
Volume.HasValue &&
|
||||||
|
PreviousClose.HasValue &&
|
||||||
|
Open.HasValue;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"Symbol={Symbol}, " +
|
||||||
|
$"LastPrice={LastPrice}, " +
|
||||||
|
$"Volume={Volume}, " +
|
||||||
|
$"PreviousClose={PreviousClose}, " +
|
||||||
|
$"Open={Open}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Program.cs
66
Program.cs
@ -11,14 +11,24 @@ namespace Backend
|
|||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var Gateway = new IBGatewayControl();
|
var Gateway = new IBGatewayControl();
|
||||||
Gateway.Start("127.0.0.1", 4002, 1);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
while (!Gateway.IsApiReady)
|
while (!Gateway.IsApiReady)
|
||||||
{
|
{
|
||||||
Thread.Sleep(50);
|
Thread.Sleep(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = new HistoricalEquityRequest
|
// ---- TEST HISTORICAL DATA REQUEST ----
|
||||||
|
|
||||||
|
var HistoricalRequest = new HistoricalEquityRequest
|
||||||
{
|
{
|
||||||
RequestId = 1,
|
RequestId = 1,
|
||||||
Symbol = "NVDA",
|
Symbol = "NVDA",
|
||||||
@ -31,15 +41,59 @@ namespace Backend
|
|||||||
FormatDate = 1
|
FormatDate = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
List<Bar> bars = Gateway.RequestHistoricalBlocking(
|
List<Bar> Bars = Gateway.RequestHistoricalBlocking(HistoricalRequest, TimeSpan.FromSeconds(15));
|
||||||
request,
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Received {Bars.Count} bars. Continue program here.");
|
||||||
|
|
||||||
|
// ---- TEST LIVE DATA REQUEST ----
|
||||||
|
|
||||||
|
var liveRequest = new LiveEquityRequest
|
||||||
|
{
|
||||||
|
RequestId = 2,
|
||||||
|
Symbol = "NVDA"
|
||||||
|
};
|
||||||
|
|
||||||
|
var live = Gateway.RequestLiveEquityBlocking(
|
||||||
|
liveRequest,
|
||||||
TimeSpan.FromSeconds(15));
|
TimeSpan.FromSeconds(15));
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine($"Received {bars.Count} bars. Continue program here.");
|
Console.WriteLine("=== Live Quote ===");
|
||||||
|
Console.WriteLine($"Symbol: {live.Symbol}");
|
||||||
|
Console.WriteLine($"Last Price: {live.LastPrice}");
|
||||||
|
Console.WriteLine($"Volume: {live.Volume}");
|
||||||
|
Console.WriteLine($"Previous Close: {live.PreviousClose}");
|
||||||
|
Console.WriteLine($"Open: {live.Open}");
|
||||||
|
|
||||||
// do next step here, for example save to database
|
|
||||||
|
|
||||||
|
// ---- FUNDAMENTALS DATA REQUEST ----
|
||||||
|
// DOES NOT WORK, NEEDS EXTRA DATA SUBSCRIPTION
|
||||||
|
/*
|
||||||
|
var fundamentalsRequest = new FundamentalsEquityRequest
|
||||||
|
{
|
||||||
|
RequestId = 3,
|
||||||
|
Symbol = "NVDA"
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fundamentals = Gateway.RequestFundamentalsBlocking(
|
||||||
|
fundamentalsRequest,
|
||||||
|
TimeSpan.FromSeconds(15));
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("=== Fundamentals ===");
|
||||||
|
Console.WriteLine($"Symbol: {fundamentals.Symbol}");
|
||||||
|
Console.WriteLine($"Market Cap: {fundamentals.MarketCap}");
|
||||||
|
Console.WriteLine($"P/E Ratio: {fundamentals.PERatio}");
|
||||||
|
Console.WriteLine($"Dividend Yield: {fundamentals.DividendYield}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Fundamentals request failed.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
Gateway.Disconnect();
|
Gateway.Disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user