remove dead options code from finnhub provider
This commit is contained in:
parent
14df3b1050
commit
78a15b89be
1 changed files with 1 additions and 274 deletions
|
|
@ -1,11 +1,8 @@
|
||||||
//! Finnhub API provider -- primary source for options chains and earnings.
|
//! Finnhub API provider -- primary source for earnings data.
|
||||||
//! API docs: https://finnhub.io/docs/api
|
//! API docs: https://finnhub.io/docs/api
|
||||||
//!
|
//!
|
||||||
//! Free tier: 60 requests/min, all US market data.
|
//! Free tier: 60 requests/min, all US market data.
|
||||||
//!
|
//!
|
||||||
//! Options endpoint: GET /api/v1/stock/option-chain?symbol=X
|
|
||||||
//! Returns all expirations with full CALL/PUT chains including greeks.
|
|
||||||
//!
|
|
||||||
//! Earnings endpoint: GET /api/v1/calendar/earnings?symbol=X&from=YYYY-MM-DD&to=YYYY-MM-DD
|
//! Earnings endpoint: GET /api/v1/calendar/earnings?symbol=X&from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||||
//! Returns historical and upcoming earnings with EPS, revenue, estimates.
|
//! Returns historical and upcoming earnings with EPS, revenue, estimates.
|
||||||
|
|
||||||
|
|
@ -13,16 +10,11 @@ const std = @import("std");
|
||||||
const http = @import("../net/http.zig");
|
const http = @import("../net/http.zig");
|
||||||
const RateLimiter = @import("../net/RateLimiter.zig");
|
const RateLimiter = @import("../net/RateLimiter.zig");
|
||||||
const Date = @import("../models/date.zig").Date;
|
const Date = @import("../models/date.zig").Date;
|
||||||
const OptionContract = @import("../models/option.zig").OptionContract;
|
|
||||||
const OptionsChain = @import("../models/option.zig").OptionsChain;
|
|
||||||
const ContractType = @import("../models/option.zig").ContractType;
|
|
||||||
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
|
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
|
||||||
const ReportTime = @import("../models/earnings.zig").ReportTime;
|
const ReportTime = @import("../models/earnings.zig").ReportTime;
|
||||||
const provider = @import("provider.zig");
|
const provider = @import("provider.zig");
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
const parseJsonFloat = json_utils.parseJsonFloat;
|
|
||||||
const optFloat = json_utils.optFloat;
|
const optFloat = json_utils.optFloat;
|
||||||
const optUint = json_utils.optUint;
|
|
||||||
const jsonStr = json_utils.jsonStr;
|
const jsonStr = json_utils.jsonStr;
|
||||||
const mapHttpError = json_utils.mapHttpError;
|
const mapHttpError = json_utils.mapHttpError;
|
||||||
|
|
||||||
|
|
@ -47,63 +39,6 @@ pub const Finnhub = struct {
|
||||||
self.client.deinit();
|
self.client.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the full options chain for a symbol (all expirations).
|
|
||||||
/// Returns chains grouped by expiration date, sorted nearest-first.
|
|
||||||
pub fn fetchOptionsChain(
|
|
||||||
self: *Finnhub,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
symbol: []const u8,
|
|
||||||
) provider.ProviderError![]OptionsChain {
|
|
||||||
self.rate_limiter.acquire();
|
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url ++ "/stock/option-chain", &.{
|
|
||||||
.{ "symbol", symbol },
|
|
||||||
.{ "token", self.api_key },
|
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(url);
|
|
||||||
|
|
||||||
var response = self.client.get(url) catch |err| return mapHttpError(err);
|
|
||||||
defer response.deinit();
|
|
||||||
|
|
||||||
return parseOptionsResponse(allocator, response.body, symbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch options for a specific expiration date only.
|
|
||||||
pub fn fetchOptionsForExpiration(
|
|
||||||
self: *Finnhub,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
symbol: []const u8,
|
|
||||||
expiration: Date,
|
|
||||||
) provider.ProviderError!?OptionsChain {
|
|
||||||
const chains = try self.fetchOptionsChain(allocator, symbol);
|
|
||||||
defer {
|
|
||||||
for (chains) |chain| {
|
|
||||||
allocator.free(chain.calls);
|
|
||||||
allocator.free(chain.puts);
|
|
||||||
}
|
|
||||||
allocator.free(chains);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (chains) |chain| {
|
|
||||||
if (chain.expiration.eql(expiration)) {
|
|
||||||
// Copy the matching chain so caller owns the memory
|
|
||||||
const calls = allocator.dupe(OptionContract, chain.calls) catch
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
errdefer allocator.free(calls);
|
|
||||||
const puts = allocator.dupe(OptionContract, chain.puts) catch
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
return OptionsChain{
|
|
||||||
.underlying_symbol = chain.underlying_symbol,
|
|
||||||
.underlying_price = chain.underlying_price,
|
|
||||||
.expiration = chain.expiration,
|
|
||||||
.calls = calls,
|
|
||||||
.puts = puts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch earnings calendar for a symbol.
|
/// Fetch earnings calendar for a symbol.
|
||||||
/// Returns earnings events sorted newest-first (upcoming first, then historical).
|
/// Returns earnings events sorted newest-first (upcoming first, then historical).
|
||||||
pub fn fetchEarnings(
|
pub fn fetchEarnings(
|
||||||
|
|
@ -153,59 +88,10 @@ pub const Finnhub = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const vtable = provider.Provider.VTable{
|
const vtable = provider.Provider.VTable{
|
||||||
.fetchOptions = @ptrCast(&fetchOptionsVtable),
|
|
||||||
.fetchEarnings = @ptrCast(&fetchEarningsVtable),
|
.fetchEarnings = @ptrCast(&fetchEarningsVtable),
|
||||||
.name = .finnhub,
|
.name = .finnhub,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn fetchOptionsVtable(
|
|
||||||
ptr: *Finnhub,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
symbol: []const u8,
|
|
||||||
expiration: ?Date,
|
|
||||||
) provider.ProviderError![]OptionContract {
|
|
||||||
if (expiration) |exp| {
|
|
||||||
const chain = try ptr.fetchOptionsForExpiration(allocator, symbol, exp);
|
|
||||||
if (chain) |c| {
|
|
||||||
// Merge calls and puts into a single slice
|
|
||||||
const total = c.calls.len + c.puts.len;
|
|
||||||
const merged = allocator.alloc(OptionContract, total) catch {
|
|
||||||
allocator.free(c.calls);
|
|
||||||
allocator.free(c.puts);
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
};
|
|
||||||
@memcpy(merged[0..c.calls.len], c.calls);
|
|
||||||
@memcpy(merged[c.calls.len..], c.puts);
|
|
||||||
allocator.free(c.calls);
|
|
||||||
allocator.free(c.puts);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
return allocator.alloc(OptionContract, 0) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
}
|
|
||||||
// No expiration given: return contracts from nearest expiration
|
|
||||||
const chains = try ptr.fetchOptionsChain(allocator, symbol);
|
|
||||||
if (chains.len == 0) {
|
|
||||||
allocator.free(chains);
|
|
||||||
return allocator.alloc(OptionContract, 0) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
}
|
|
||||||
defer {
|
|
||||||
for (chains[1..]) |chain| {
|
|
||||||
allocator.free(chain.calls);
|
|
||||||
allocator.free(chain.puts);
|
|
||||||
}
|
|
||||||
allocator.free(chains);
|
|
||||||
}
|
|
||||||
const first = chains[0];
|
|
||||||
const total = first.calls.len + first.puts.len;
|
|
||||||
const merged = allocator.alloc(OptionContract, total) catch
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
@memcpy(merged[0..first.calls.len], first.calls);
|
|
||||||
@memcpy(merged[first.calls.len..], first.puts);
|
|
||||||
allocator.free(first.calls);
|
|
||||||
allocator.free(first.puts);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetchEarningsVtable(
|
fn fetchEarningsVtable(
|
||||||
ptr: *Finnhub,
|
ptr: *Finnhub,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
@ -217,122 +103,6 @@ pub const Finnhub = struct {
|
||||||
|
|
||||||
// -- JSON parsing --
|
// -- JSON parsing --
|
||||||
|
|
||||||
fn parseOptionsResponse(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
body: []const u8,
|
|
||||||
symbol: []const u8,
|
|
||||||
) provider.ProviderError![]OptionsChain {
|
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
|
||||||
return provider.ProviderError.ParseError;
|
|
||||||
defer parsed.deinit();
|
|
||||||
|
|
||||||
const root = parsed.value.object;
|
|
||||||
|
|
||||||
// Check for error response
|
|
||||||
if (root.get("error")) |_| return provider.ProviderError.RequestFailed;
|
|
||||||
|
|
||||||
const underlying_price: f64 = if (root.get("lastTradePrice")) |v| parseJsonFloat(v) else 0;
|
|
||||||
|
|
||||||
const data_arr = root.get("data") orelse {
|
|
||||||
const empty = allocator.alloc(OptionsChain, 0) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
return empty;
|
|
||||||
};
|
|
||||||
const items = switch (data_arr) {
|
|
||||||
.array => |a| a.items,
|
|
||||||
else => {
|
|
||||||
const empty = allocator.alloc(OptionsChain, 0) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
return empty;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var chains: std.ArrayList(OptionsChain) = .empty;
|
|
||||||
errdefer {
|
|
||||||
for (chains.items) |chain| {
|
|
||||||
allocator.free(chain.calls);
|
|
||||||
allocator.free(chain.puts);
|
|
||||||
}
|
|
||||||
chains.deinit(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (items) |item| {
|
|
||||||
const obj = switch (item) {
|
|
||||||
.object => |o| o,
|
|
||||||
else => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
const exp_str = jsonStr(obj.get("expirationDate")) orelse continue;
|
|
||||||
const expiration = Date.parse(exp_str) catch continue;
|
|
||||||
|
|
||||||
const options_obj = (obj.get("options") orelse continue);
|
|
||||||
const options = switch (options_obj) {
|
|
||||||
.object => |o| o,
|
|
||||||
else => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
const calls = parseContracts(allocator, options.get("CALL"), .call, expiration) catch
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
errdefer allocator.free(calls);
|
|
||||||
const puts = parseContracts(allocator, options.get("PUT"), .put, expiration) catch
|
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
|
|
||||||
chains.append(allocator, .{
|
|
||||||
.underlying_symbol = symbol,
|
|
||||||
.underlying_price = underlying_price,
|
|
||||||
.expiration = expiration,
|
|
||||||
.calls = calls,
|
|
||||||
.puts = puts,
|
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chains.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseContracts(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
val: ?std.json.Value,
|
|
||||||
contract_type: ContractType,
|
|
||||||
expiration: Date,
|
|
||||||
) ![]OptionContract {
|
|
||||||
const arr = val orelse {
|
|
||||||
return try allocator.alloc(OptionContract, 0);
|
|
||||||
};
|
|
||||||
const items = switch (arr) {
|
|
||||||
.array => |a| a.items,
|
|
||||||
else => return try allocator.alloc(OptionContract, 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
var contracts: std.ArrayList(OptionContract) = .empty;
|
|
||||||
errdefer contracts.deinit(allocator);
|
|
||||||
|
|
||||||
for (items) |item| {
|
|
||||||
const obj = switch (item) {
|
|
||||||
.object => |o| o,
|
|
||||||
else => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
const strike = parseJsonFloat(obj.get("strike") orelse continue);
|
|
||||||
if (strike <= 0) continue;
|
|
||||||
|
|
||||||
contracts.append(allocator, .{
|
|
||||||
.contract_type = contract_type,
|
|
||||||
.strike = strike,
|
|
||||||
.expiration = expiration,
|
|
||||||
.bid = optFloat(obj.get("bid")),
|
|
||||||
.ask = optFloat(obj.get("ask")),
|
|
||||||
.last_price = optFloat(obj.get("lastPrice")),
|
|
||||||
.volume = optUint(obj.get("volume")),
|
|
||||||
.open_interest = optUint(obj.get("openInterest")),
|
|
||||||
.implied_volatility = optFloat(obj.get("impliedVolatility")),
|
|
||||||
.delta = optFloat(obj.get("delta")),
|
|
||||||
.gamma = optFloat(obj.get("gamma")),
|
|
||||||
.theta = optFloat(obj.get("theta")),
|
|
||||||
.vega = optFloat(obj.get("vega")),
|
|
||||||
}) catch return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
return contracts.toOwnedSlice(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseEarningsResponse(
|
fn parseEarningsResponse(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
|
|
@ -498,46 +268,3 @@ test "parseEarningsResponse empty" {
|
||||||
defer allocator.free(events);
|
defer allocator.free(events);
|
||||||
try std.testing.expectEqual(@as(usize, 0), events.len);
|
try std.testing.expectEqual(@as(usize, 0), events.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseOptionsResponse basic" {
|
|
||||||
const body =
|
|
||||||
\\{
|
|
||||||
\\ "lastTradePrice": 185.50,
|
|
||||||
\\ "data": [
|
|
||||||
\\ {
|
|
||||||
\\ "expirationDate": "2026-03-20",
|
|
||||||
\\ "options": {
|
|
||||||
\\ "CALL": [
|
|
||||||
\\ {"strike": 180.0, "bid": 7.50, "ask": 7.80, "volume": 1234, "openInterest": 5678}
|
|
||||||
\\ ],
|
|
||||||
\\ "PUT": [
|
|
||||||
\\ {"strike": 180.0, "bid": 2.10, "ask": 2.30}
|
|
||||||
\\ ]
|
|
||||||
\\ }
|
|
||||||
\\ }
|
|
||||||
\\ ]
|
|
||||||
\\}
|
|
||||||
;
|
|
||||||
|
|
||||||
const allocator = std.testing.allocator;
|
|
||||||
const chains = try parseOptionsResponse(allocator, body, "AAPL");
|
|
||||||
defer {
|
|
||||||
for (chains) |chain| {
|
|
||||||
allocator.free(chain.calls);
|
|
||||||
allocator.free(chain.puts);
|
|
||||||
}
|
|
||||||
allocator.free(chains);
|
|
||||||
}
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 1), chains.len);
|
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 185.50), chains[0].underlying_price.?, 0.01);
|
|
||||||
try std.testing.expect(chains[0].expiration.eql(Date.fromYmd(2026, 3, 20)));
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 1), chains[0].calls.len);
|
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 180.0), chains[0].calls[0].strike, 0.01);
|
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 7.50), chains[0].calls[0].bid.?, 0.01);
|
|
||||||
try std.testing.expectEqual(@as(?u64, 1234), chains[0].calls[0].volume);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 1), chains[0].puts.len);
|
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 2.10), chains[0].puts[0].bid.?, 0.01);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue