remove unused provider interface stuff

This commit is contained in:
Emil Lerch 2026-03-06 15:56:09 -08:00
parent 6a680a2381
commit 26b831631d
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 123 additions and 404 deletions

View file

@ -13,10 +13,8 @@ const Date = @import("../models/date.zig").Date;
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
const Holding = @import("../models/etf_profile.zig").Holding;
const SectorWeight = @import("../models/etf_profile.zig").SectorWeight;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig");
const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://www.alphavantage.co/query";
@ -116,7 +114,7 @@ test "parseEtfProfileResponse error response" {
const allocator = std.testing.allocator;
const result = parseEtfProfileResponse(allocator, body, "BAD");
try std.testing.expectError(provider.ProviderError.RequestFailed, result);
try std.testing.expectError(error.RequestFailed, result);
}
test "parseEtfProfileResponse rate limited" {
@ -126,7 +124,7 @@ test "parseEtfProfileResponse rate limited" {
const allocator = std.testing.allocator;
const result = parseEtfProfileResponse(allocator, body, "SPY");
try std.testing.expectError(provider.ProviderError.RateLimited, result);
try std.testing.expectError(error.RateLimited, result);
}
test "parseCompanyOverview basic" {
@ -201,17 +199,17 @@ pub const AlphaVantage = struct {
self: *AlphaVantage,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError!CompanyOverview {
) !CompanyOverview {
self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url, &.{
const url = try http.buildUrl(allocator, base_url, &.{
.{ "function", "OVERVIEW" },
.{ "symbol", symbol },
.{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory;
});
defer allocator.free(url);
var response = self.client.get(url) catch |err| return mapHttpError(err);
var response = try self.client.get(url);
defer response.deinit();
return parseCompanyOverview(allocator, response.body, symbol);
@ -222,41 +220,21 @@ pub const AlphaVantage = struct {
self: *AlphaVantage,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError!EtfProfile {
) !EtfProfile {
self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url, &.{
const url = try http.buildUrl(allocator, base_url, &.{
.{ "function", "ETF_PROFILE" },
.{ "symbol", symbol },
.{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory;
});
defer allocator.free(url);
var response = self.client.get(url) catch |err| return mapHttpError(err);
var response = try self.client.get(url);
defer response.deinit();
return parseEtfProfileResponse(allocator, response.body, symbol);
}
pub fn asProvider(self: *AlphaVantage) provider.Provider {
return .{
.ptr = @ptrCast(self),
.vtable = &vtable,
};
}
const vtable = provider.Provider.VTable{
.fetchEtfProfile = @ptrCast(&fetchEtfProfileVtable),
.name = .alphavantage,
};
fn fetchEtfProfileVtable(
ptr: *AlphaVantage,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError!EtfProfile {
return ptr.fetchEtfProfile(allocator, symbol);
}
};
// -- JSON parsing --
@ -265,17 +243,17 @@ fn parseEtfProfileResponse(
allocator: std.mem.Allocator,
body: []const u8,
symbol: []const u8,
) provider.ProviderError!EtfProfile {
) !EtfProfile {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
// Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed;
if (root.get("Note")) |_| return provider.ProviderError.RateLimited;
if (root.get("Information")) |_| return provider.ProviderError.RateLimited;
if (root.get("Error Message")) |_| return error.RequestFailed;
if (root.get("Note")) |_| return error.RateLimited;
if (root.get("Information")) |_| return error.RateLimited;
var profile = EtfProfile{
.symbol = symbol,
@ -318,13 +296,13 @@ fn parseEtfProfileResponse(
const name = jsonStr(obj.get("sector")) orelse continue;
const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
const duped_name = allocator.dupe(u8, name) catch return provider.ProviderError.OutOfMemory;
sectors.append(allocator, .{
const duped_name = try allocator.dupe(u8, name);
try sectors.append(allocator, .{
.name = duped_name,
.weight = weight,
}) catch return provider.ProviderError.OutOfMemory;
});
}
profile.sectors = sectors.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
profile.sectors = try sectors.toOwnedSlice(allocator);
}
}
@ -348,18 +326,18 @@ fn parseEtfProfileResponse(
const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
const duped_sym = if (jsonStr(obj.get("symbol"))) |s|
(allocator.dupe(u8, s) catch return provider.ProviderError.OutOfMemory)
(try allocator.dupe(u8, s))
else
null;
const duped_name = allocator.dupe(u8, desc) catch return provider.ProviderError.OutOfMemory;
const duped_name = try allocator.dupe(u8, desc);
holdings.append(allocator, .{
try holdings.append(allocator, .{
.symbol = duped_sym,
.name = duped_name,
.weight = weight,
}) catch return provider.ProviderError.OutOfMemory;
});
}
profile.holdings = holdings.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
profile.holdings = try holdings.toOwnedSlice(allocator);
}
}
@ -383,16 +361,16 @@ fn parseCompanyOverview(
allocator: std.mem.Allocator,
body: []const u8,
symbol: []const u8,
) provider.ProviderError!CompanyOverview {
) !CompanyOverview {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed;
if (root.get("Note")) |_| return provider.ProviderError.RateLimited;
if (root.get("Information")) |_| return provider.ProviderError.RateLimited;
if (root.get("Error Message")) |_| return error.RequestFailed;
if (root.get("Note")) |_| return error.RateLimited;
if (root.get("Information")) |_| return error.RateLimited;
return .{
.symbol = symbol,

View file

@ -11,11 +11,9 @@ 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 provider = @import("provider.zig");
const json_utils = @import("json_utils.zig");
const optFloat = json_utils.optFloat;
const optUint = json_utils.optUint;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options";
@ -42,14 +40,14 @@ pub const Cboe = struct {
self: *Cboe,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError![]OptionsChain {
) ![]OptionsChain {
self.rate_limiter.acquire();
// Build URL: {base_url}/{SYMBOL}.json
const url = buildCboeUrl(allocator, symbol) catch return provider.ProviderError.OutOfMemory;
const url = try buildCboeUrl(allocator, symbol);
defer allocator.free(url);
var response = self.client.get(url) catch |err| return mapHttpError(err);
var response = try self.client.get(url);
defer response.deinit();
return parseResponse(allocator, response.body, symbol);
@ -73,26 +71,26 @@ fn parseResponse(
allocator: std.mem.Allocator,
body: []const u8,
symbol: []const u8,
) provider.ProviderError![]OptionsChain {
) ![]OptionsChain {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = switch (parsed.value) {
.object => |o| o,
else => return provider.ProviderError.ParseError,
else => return error.ParseError,
};
const data_obj = switch (root.get("data") orelse return provider.ProviderError.ParseError) {
const data_obj = switch (root.get("data") orelse return error.ParseError) {
.object => |o| o,
else => return provider.ProviderError.ParseError,
else => return error.ParseError,
};
const underlying_price: ?f64 = if (data_obj.get("current_price")) |v| optFloat(v) else null;
const options_arr = switch (data_obj.get("options") orelse return provider.ProviderError.ParseError) {
const options_arr = switch (data_obj.get("options") orelse return error.ParseError) {
.array => |a| a.items,
else => return provider.ProviderError.ParseError,
else => return error.ParseError,
};
// Parse all contracts and group by expiration.
@ -129,12 +127,11 @@ fn parseResponse(
};
// Find or create the expiration bucket
const bucket = exp_map.getOrPut(allocator, occ.expiration) catch
return provider.ProviderError.OutOfMemory;
const bucket = try exp_map.getOrPut(allocator, occ.expiration);
switch (occ.contract_type) {
.call => bucket.calls.append(allocator, contract) catch return provider.ProviderError.OutOfMemory,
.put => bucket.puts.append(allocator, contract) catch return provider.ProviderError.OutOfMemory,
.call => try bucket.calls.append(allocator, contract),
.put => try bucket.puts.append(allocator, contract),
}
}
@ -220,7 +217,7 @@ const ExpMap = struct {
allocator: std.mem.Allocator,
symbol: []const u8,
underlying_price: ?f64,
) provider.ProviderError![]OptionsChain {
) ![]OptionsChain {
// Sort entries by expiration
std.mem.sort(Entry, self.entries.items, {}, struct {
fn lessThan(_: void, a: Entry, b: Entry) bool {
@ -228,8 +225,7 @@ const ExpMap = struct {
}
}.lessThan);
var chains = allocator.alloc(OptionsChain, self.entries.items.len) catch
return provider.ProviderError.OutOfMemory;
var chains = try allocator.alloc(OptionsChain, self.entries.items.len);
var initialized: usize = 0;
errdefer {
@ -242,14 +238,11 @@ const ExpMap = struct {
}
for (self.entries.items, 0..) |*entry, i| {
const owned_symbol = allocator.dupe(u8, symbol) catch
return provider.ProviderError.OutOfMemory;
const owned_symbol = try allocator.dupe(u8, symbol);
errdefer allocator.free(owned_symbol);
const calls = entry.calls.toOwnedSlice(allocator) catch
return provider.ProviderError.OutOfMemory;
const calls = try entry.calls.toOwnedSlice(allocator);
errdefer allocator.free(calls);
const puts = entry.puts.toOwnedSlice(allocator) catch
return provider.ProviderError.OutOfMemory;
const puts = try entry.puts.toOwnedSlice(allocator);
chains[i] = .{
.underlying_symbol = owned_symbol,
@ -351,5 +344,5 @@ test "parseResponse missing data" {
const allocator = std.testing.allocator;
const result = parseResponse(allocator, body, "AAPL");
try std.testing.expectError(provider.ProviderError.ParseError, result);
try std.testing.expectError(error.ParseError, result);
}

View file

@ -12,11 +12,9 @@ const RateLimiter = @import("../net/RateLimiter.zig");
const Date = @import("../models/date.zig").Date;
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
const ReportTime = @import("../models/earnings.zig").ReportTime;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig");
const optFloat = json_utils.optFloat;
const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://finnhub.io/api/v1";
@ -47,7 +45,7 @@ pub const Finnhub = struct {
symbol: []const u8,
from: ?Date,
to: ?Date,
) provider.ProviderError![]EarningsEvent {
) ![]EarningsEvent {
self.rate_limiter.acquire();
var params: [4][2][]const u8 = undefined;
@ -70,35 +68,14 @@ pub const Finnhub = struct {
n += 1;
}
const url = http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]) catch
return provider.ProviderError.OutOfMemory;
const url = try http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]);
defer allocator.free(url);
var response = self.client.get(url) catch |err| return mapHttpError(err);
var response = try self.client.get(url);
defer response.deinit();
return parseEarningsResponse(allocator, response.body, symbol);
}
pub fn asProvider(self: *Finnhub) provider.Provider {
return .{
.ptr = @ptrCast(self),
.vtable = &vtable,
};
}
const vtable = provider.Provider.VTable{
.fetchEarnings = @ptrCast(&fetchEarningsVtable),
.name = .finnhub,
};
fn fetchEarningsVtable(
ptr: *Finnhub,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError![]EarningsEvent {
return ptr.fetchEarnings(allocator, symbol, null, null);
}
};
// -- JSON parsing --
@ -107,24 +84,22 @@ fn parseEarningsResponse(
allocator: std.mem.Allocator,
body: []const u8,
symbol: []const u8,
) provider.ProviderError![]EarningsEvent {
) ![]EarningsEvent {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
if (root.get("error")) |_| return provider.ProviderError.RequestFailed;
if (root.get("error")) |_| return error.RequestFailed;
const cal = root.get("earningsCalendar") orelse {
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(EarningsEvent, 0);
};
const items = switch (cal) {
.array => |a| a.items,
else => {
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(EarningsEvent, 0);
},
};
@ -151,7 +126,7 @@ fn parseEarningsResponse(
else
null;
events.append(allocator, .{
try events.append(allocator, .{
.symbol = symbol,
.date = date,
.estimate = estimate,
@ -163,10 +138,10 @@ fn parseEarningsResponse(
.revenue_actual = optFloat(obj.get("revenueActual")),
.revenue_estimate = optFloat(obj.get("revenueEstimate")),
.report_time = parseReportTime(obj.get("hour")),
}) catch return provider.ProviderError.OutOfMemory;
});
}
return events.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
return try events.toOwnedSlice(allocator);
}
// -- Helpers --
@ -255,7 +230,7 @@ test "parseEarningsResponse error" {
const allocator = std.testing.allocator;
const result = parseEarningsResponse(allocator, body, "AAPL");
try std.testing.expectError(provider.ProviderError.RequestFailed, result);
try std.testing.expectError(error.RequestFailed, result);
}
test "parseEarningsResponse empty" {

View file

@ -1,10 +1,8 @@
//! Shared JSON parsing helpers used by all API providers.
//! Centralises the common patterns: extracting floats, strings,
//! unsigned ints, and mapping HTTP errors to provider errors.
//! unsigned ints, and mapping HTTP errors.
const std = @import("std");
const http = @import("../net/http.zig");
const provider = @import("provider.zig");
/// Extract a required float from a JSON value (string, float, or integer).
/// Returns 0 for null, missing, or unparseable values.
@ -49,13 +47,3 @@ pub fn jsonStr(val: ?std.json.Value) ?[]const u8 {
else => null,
};
}
/// Map an HTTP-level error to the corresponding provider error.
pub fn mapHttpError(err: http.HttpError) provider.ProviderError {
return switch (err) {
error.RateLimited => provider.ProviderError.RateLimited,
error.Unauthorized => provider.ProviderError.Unauthorized,
error.NotFound => provider.ProviderError.NotFound,
else => provider.ProviderError.RequestFailed,
};
}

View file

@ -13,11 +13,9 @@ const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend;
const DividendType = @import("../models/dividend.zig").DividendType;
const Split = @import("../models/split.zig").Split;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat;
const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://api.polygon.io";
@ -48,7 +46,7 @@ pub const Polygon = struct {
symbol: []const u8,
from: ?Date,
to: ?Date,
) provider.ProviderError![]Dividend {
) ![]Dividend {
var all_dividends: std.ArrayList(Dividend) = .empty;
errdefer {
for (all_dividends.items) |d| d.deinit(allocator);
@ -84,15 +82,13 @@ pub const Polygon = struct {
n += 1;
}
const url = http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]) catch
return provider.ProviderError.OutOfMemory;
const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]);
defer allocator.free(url);
const authed = appendApiKey(allocator, url, self.api_key) catch
return provider.ProviderError.OutOfMemory;
const authed = try appendApiKey(allocator, url, self.api_key);
defer allocator.free(authed);
var response = self.client.get(authed) catch |err| return mapHttpError(err);
var response = try self.client.get(authed);
defer response.deinit();
next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
@ -102,18 +98,17 @@ pub const Polygon = struct {
while (next_url) |cursor_url| {
self.rate_limiter.acquire();
const authed = appendApiKey(allocator, cursor_url, self.api_key) catch
return provider.ProviderError.OutOfMemory;
const authed = try appendApiKey(allocator, cursor_url, self.api_key);
defer allocator.free(authed);
var response = self.client.get(authed) catch |err| return mapHttpError(err);
var response = try self.client.get(authed);
defer response.deinit();
allocator.free(cursor_url);
next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
}
return all_dividends.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
return try all_dividends.toOwnedSlice(allocator);
}
/// Fetch split history for a ticker. Results sorted oldest-first.
@ -122,21 +117,20 @@ pub const Polygon = struct {
self: *Polygon,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError![]Split {
) ![]Split {
self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url ++ "/v3/reference/splits", &.{
const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/splits", &.{
.{ "ticker", symbol },
.{ "limit", "1000" },
.{ "sort", "execution_date" },
}) catch return provider.ProviderError.OutOfMemory;
});
defer allocator.free(url);
const authed = appendApiKey(allocator, url, self.api_key) catch
return provider.ProviderError.OutOfMemory;
const authed = try appendApiKey(allocator, url, self.api_key);
defer allocator.free(authed);
var response = self.client.get(authed) catch |err| return mapHttpError(err);
var response = try self.client.get(authed);
defer response.deinit();
return parseSplitsResponse(allocator, response.body);
@ -150,7 +144,7 @@ pub const Polygon = struct {
symbol: []const u8,
from: Date,
to: Date,
) provider.ProviderError![]Candle {
) ![]Candle {
self.rate_limiter.acquire();
var from_buf: [10]u8 = undefined;
@ -159,46 +153,21 @@ pub const Polygon = struct {
const to_str = to.format(&to_buf);
// Build URL manually since the path contains the date range
const path = std.fmt.allocPrint(
const path = try std.fmt.allocPrint(
allocator,
"{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000",
.{ base_url, symbol, from_str, to_str },
) catch return provider.ProviderError.OutOfMemory;
);
defer allocator.free(path);
const authed = appendApiKey(allocator, path, self.api_key) catch
return provider.ProviderError.OutOfMemory;
const authed = try appendApiKey(allocator, path, self.api_key);
defer allocator.free(authed);
var response = self.client.get(authed) catch |err| return mapHttpError(err);
var response = try self.client.get(authed);
defer response.deinit();
return parseCandlesResponse(allocator, response.body);
}
pub fn asProvider(self: *Polygon) provider.Provider {
return .{
.ptr = @ptrCast(self),
.vtable = &vtable,
};
}
const vtable = provider.Provider.VTable{
.fetchDividends = @ptrCast(&fetchDividendsVtable),
.fetchSplits = @ptrCast(&fetchSplitsVtable),
.fetchCandles = @ptrCast(&fetchCandlesVtable),
.name = .polygon,
};
fn fetchDividendsVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8, from: ?Date, to: ?Date) provider.ProviderError![]Dividend {
return ptr.fetchDividends(allocator, symbol, from, to);
}
fn fetchSplitsVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8) provider.ProviderError![]Split {
return ptr.fetchSplits(allocator, symbol);
}
fn fetchCandlesVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8, from: Date, to: Date) provider.ProviderError![]Candle {
return ptr.fetchCandles(allocator, symbol, from, to);
}
};
// -- JSON parsing --
@ -207,9 +176,9 @@ fn parseDividendsPage(
allocator: std.mem.Allocator,
body: []const u8,
out: *std.ArrayList(Dividend),
) provider.ProviderError!?[]const u8 {
) !?[]const u8 {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
@ -217,7 +186,7 @@ fn parseDividendsPage(
// Check status
if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
return provider.ProviderError.RequestFailed;
return error.RequestFailed;
}
const results = root.get("results") orelse return null;
@ -244,7 +213,7 @@ fn parseDividendsPage(
const amount = parseJsonFloat(obj.get("cash_amount"));
if (amount <= 0) continue;
out.append(allocator, .{
try out.append(allocator, .{
.ex_date = ex_date,
.amount = amount,
.pay_date = parseDateField(obj, "pay_date"),
@ -255,7 +224,7 @@ fn parseDividendsPage(
(allocator.dupe(u8, s) catch null)
else
null,
}) catch return provider.ProviderError.OutOfMemory;
});
}
// Check for next_url (pagination cursor)
@ -264,34 +233,32 @@ fn parseDividendsPage(
.string => |s| s,
else => return null,
};
const duped = allocator.dupe(u8, url_str) catch return provider.ProviderError.OutOfMemory;
const duped = try allocator.dupe(u8, url_str);
return duped;
}
return null;
}
fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Split {
fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) ![]Split {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
return provider.ProviderError.RequestFailed;
return error.RequestFailed;
}
const results = root.get("results") orelse {
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(Split, 0);
};
const items = switch (results) {
.array => |a| a.items,
else => {
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(Split, 0);
},
};
@ -313,37 +280,35 @@ fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider.
break :blk Date.parse(s) catch continue;
};
splits.append(allocator, .{
try splits.append(allocator, .{
.date = date,
.numerator = parseJsonFloat(obj.get("split_to")),
.denominator = parseJsonFloat(obj.get("split_from")),
}) catch return provider.ProviderError.OutOfMemory;
});
}
return splits.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
return try splits.toOwnedSlice(allocator);
}
fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Candle {
fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Candle {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
return provider.ProviderError.RequestFailed;
return error.RequestFailed;
}
const results = root.get("results") orelse {
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(Candle, 0);
};
const items = switch (results) {
.array => |a| a.items,
else => {
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory;
return empty;
return try allocator.alloc(Candle, 0);
},
};
@ -370,7 +335,7 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
const close = parseJsonFloat(obj.get("c"));
candles.append(allocator, .{
try candles.append(allocator, .{
.date = date,
.open = parseJsonFloat(obj.get("o")),
.high = parseJsonFloat(obj.get("h")),
@ -378,10 +343,10 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
.close = close,
.adj_close = close, // Polygon adjusted=true gives adjusted values
.volume = @intFromFloat(parseJsonFloat(obj.get("v"))),
}) catch return provider.ProviderError.OutOfMemory;
});
}
return candles.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
return try candles.toOwnedSlice(allocator);
}
// -- Helpers --
@ -506,7 +471,7 @@ test "parseDividendsPage error status" {
}
const result = parseDividendsPage(allocator, body, &out);
try std.testing.expectError(provider.ProviderError.RequestFailed, result);
try std.testing.expectError(error.RequestFailed, result);
}
test "parseSplitsResponse basic" {

View file

@ -1,147 +0,0 @@
const std = @import("std");
const Date = @import("../models/date.zig").Date;
const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend;
const Split = @import("../models/split.zig").Split;
const OptionContract = @import("../models/option.zig").OptionContract;
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
pub const ProviderError = error{
ApiKeyMissing,
RequestFailed,
RateLimited,
ParseError,
NotSupported,
OutOfMemory,
Unauthorized,
NotFound,
ServerError,
InvalidResponse,
ConnectionRefused,
};
/// Identifies which upstream data source a result came from.
pub const ProviderName = enum {
twelvedata,
polygon,
finnhub,
cboe,
alphavantage,
};
/// Common interface for all data providers.
/// Each provider implements the capabilities it supports and returns
/// `error.NotSupported` for those it doesn't.
pub const Provider = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
fetchCandles: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
from: Date,
to: Date,
) ProviderError![]Candle = null,
fetchDividends: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
from: ?Date,
to: ?Date,
) ProviderError![]Dividend = null,
fetchSplits: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError![]Split = null,
fetchOptions: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
expiration: ?Date,
) ProviderError![]OptionContract = null,
fetchEarnings: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError![]EarningsEvent = null,
fetchEtfProfile: ?*const fn (
ptr: *anyopaque,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError!EtfProfile = null,
name: ProviderName,
};
pub fn fetchCandles(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
from: Date,
to: Date,
) ProviderError![]Candle {
const func = self.vtable.fetchCandles orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol, from, to);
}
pub fn fetchDividends(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
from: ?Date,
to: ?Date,
) ProviderError![]Dividend {
const func = self.vtable.fetchDividends orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol, from, to);
}
pub fn fetchSplits(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError![]Split {
const func = self.vtable.fetchSplits orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol);
}
pub fn fetchOptions(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
expiration: ?Date,
) ProviderError![]OptionContract {
const func = self.vtable.fetchOptions orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol, expiration);
}
pub fn fetchEarnings(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError![]EarningsEvent {
const func = self.vtable.fetchEarnings orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol);
}
pub fn fetchEtfProfile(
self: Provider,
allocator: std.mem.Allocator,
symbol: []const u8,
) ProviderError!EtfProfile {
const func = self.vtable.fetchEtfProfile orelse return ProviderError.NotSupported;
return func(self.ptr, allocator, symbol);
}
pub fn providerName(self: Provider) ProviderName {
return self.vtable.name;
}
};

View file

@ -12,7 +12,6 @@ const http = @import("../net/http.zig");
const RateLimiter = @import("../net/RateLimiter.zig");
const Date = @import("../models/date.zig").Date;
const Candle = @import("../models/candle.zig").Candle;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat;
@ -45,7 +44,7 @@ pub const TwelveData = struct {
symbol: []const u8,
from: Date,
to: Date,
) provider.ProviderError![]Candle {
) ![]Candle {
self.rate_limiter.acquire();
var from_buf: [10]u8 = undefined;
@ -53,22 +52,17 @@ pub const TwelveData = struct {
const from_str = from.format(&from_buf);
const to_str = to.format(&to_buf);
const url = http.buildUrl(allocator, base_url ++ "/time_series", &.{
const url = try http.buildUrl(allocator, base_url ++ "/time_series", &.{
.{ "symbol", symbol },
.{ "interval", "1day" },
.{ "start_date", from_str },
.{ "end_date", to_str },
.{ "outputsize", "5000" },
.{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory;
});
defer allocator.free(url);
var response = self.client.get(url) catch |err| return switch (err) {
error.RateLimited => provider.ProviderError.RateLimited,
error.Unauthorized => provider.ProviderError.Unauthorized,
error.NotFound => provider.ProviderError.NotFound,
else => provider.ProviderError.RequestFailed,
};
var response = try self.client.get(url);
defer response.deinit();
return parseTimeSeriesResponse(allocator, response.body);
@ -84,7 +78,7 @@ pub const TwelveData = struct {
/// Parse and print quote data. Caller should use this within the
/// lifetime of the QuoteResponse.
pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) provider.ProviderError!ParsedQuote {
pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) !ParsedQuote {
return parseQuoteBody(allocator, self.body);
}
};
@ -160,21 +154,16 @@ pub const TwelveData = struct {
self: *TwelveData,
allocator: std.mem.Allocator,
symbol: []const u8,
) provider.ProviderError!QuoteResponse {
) !QuoteResponse {
self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url ++ "/quote", &.{
const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{
.{ "symbol", symbol },
.{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory;
});
defer allocator.free(url);
var response = self.client.get(url) catch |err| return switch (err) {
error.RateLimited => provider.ProviderError.RateLimited,
error.Unauthorized => provider.ProviderError.Unauthorized,
error.NotFound => provider.ProviderError.NotFound,
else => provider.ProviderError.RequestFailed,
};
var response = try self.client.get(url);
// Transfer ownership of body to QuoteResponse
const body = response.body;
@ -185,35 +174,13 @@ pub const TwelveData = struct {
.allocator = allocator,
};
}
pub fn asProvider(self: *TwelveData) provider.Provider {
return .{
.ptr = @ptrCast(self),
.vtable = &vtable,
};
}
const vtable = provider.Provider.VTable{
.fetchCandles = @ptrCast(&fetchCandlesVtable),
.name = .twelvedata,
};
fn fetchCandlesVtable(
ptr: *TwelveData,
allocator: std.mem.Allocator,
symbol: []const u8,
from: Date,
to: Date,
) provider.ProviderError![]Candle {
return ptr.fetchCandles(allocator, symbol, from, to);
}
};
// -- JSON parsing --
fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Candle {
fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Candle {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
defer parsed.deinit();
const root = parsed.value;
@ -224,18 +191,18 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi
if (std.mem.eql(u8, status.string, "error")) {
// Check error code
if (root.object.get("code")) |code| {
if (code == .integer and code.integer == 429) return provider.ProviderError.RateLimited;
if (code == .integer and code.integer == 401) return provider.ProviderError.Unauthorized;
if (code == .integer and code.integer == 429) return error.RateLimited;
if (code == .integer and code.integer == 401) return error.Unauthorized;
}
return provider.ProviderError.RequestFailed;
return error.RequestFailed;
}
}
}
const values_json = root.object.get("values") orelse return provider.ProviderError.ParseError;
const values_json = root.object.get("values") orelse return error.ParseError;
const values = switch (values_json) {
.array => |a| a.items,
else => return provider.ProviderError.ParseError,
else => return error.ParseError,
};
// Twelve Data returns newest first. We'll parse into a list and reverse.
@ -259,7 +226,7 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi
break :blk Date.parse(date_part) catch continue;
};
candles.append(allocator, .{
try candles.append(allocator, .{
.date = date,
.open = parseJsonFloat(obj.get("open")),
.high = parseJsonFloat(obj.get("high")),
@ -268,25 +235,25 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi
// Twelve Data close is split-adjusted only, not dividend-adjusted
.adj_close = parseJsonFloat(obj.get("close")),
.volume = @intFromFloat(parseJsonFloat(obj.get("volume"))),
}) catch return provider.ProviderError.OutOfMemory;
});
}
// Reverse to get oldest-first ordering
const slice = candles.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory;
const slice = try candles.toOwnedSlice(allocator);
std.mem.reverse(Candle, slice);
return slice;
}
fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError!TwelveData.ParsedQuote {
fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) !TwelveData.ParsedQuote {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError;
return error.ParseError;
// Check for error
if (parsed.value.object.get("status")) |status| {
if (status == .string and std.mem.eql(u8, status.string, "error")) {
var p = parsed;
p.deinit();
return provider.ProviderError.RequestFailed;
return error.RequestFailed;
}
}