remove unused provider interface stuff
This commit is contained in:
parent
6a680a2381
commit
26b831631d
7 changed files with 123 additions and 404 deletions
|
|
@ -13,10 +13,8 @@ const Date = @import("../models/date.zig").Date;
|
||||||
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
|
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
|
||||||
const Holding = @import("../models/etf_profile.zig").Holding;
|
const Holding = @import("../models/etf_profile.zig").Holding;
|
||||||
const SectorWeight = @import("../models/etf_profile.zig").SectorWeight;
|
const SectorWeight = @import("../models/etf_profile.zig").SectorWeight;
|
||||||
const provider = @import("provider.zig");
|
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
const jsonStr = json_utils.jsonStr;
|
const jsonStr = json_utils.jsonStr;
|
||||||
const mapHttpError = json_utils.mapHttpError;
|
|
||||||
|
|
||||||
const base_url = "https://www.alphavantage.co/query";
|
const base_url = "https://www.alphavantage.co/query";
|
||||||
|
|
||||||
|
|
@ -116,7 +114,7 @@ test "parseEtfProfileResponse error response" {
|
||||||
|
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const result = parseEtfProfileResponse(allocator, body, "BAD");
|
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" {
|
test "parseEtfProfileResponse rate limited" {
|
||||||
|
|
@ -126,7 +124,7 @@ test "parseEtfProfileResponse rate limited" {
|
||||||
|
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const result = parseEtfProfileResponse(allocator, body, "SPY");
|
const result = parseEtfProfileResponse(allocator, body, "SPY");
|
||||||
try std.testing.expectError(provider.ProviderError.RateLimited, result);
|
try std.testing.expectError(error.RateLimited, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseCompanyOverview basic" {
|
test "parseCompanyOverview basic" {
|
||||||
|
|
@ -201,17 +199,17 @@ pub const AlphaVantage = struct {
|
||||||
self: *AlphaVantage,
|
self: *AlphaVantage,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError!CompanyOverview {
|
) !CompanyOverview {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url, &.{
|
const url = try http.buildUrl(allocator, base_url, &.{
|
||||||
.{ "function", "OVERVIEW" },
|
.{ "function", "OVERVIEW" },
|
||||||
.{ "symbol", symbol },
|
.{ "symbol", symbol },
|
||||||
.{ "apikey", self.api_key },
|
.{ "apikey", self.api_key },
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
defer allocator.free(url);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseCompanyOverview(allocator, response.body, symbol);
|
return parseCompanyOverview(allocator, response.body, symbol);
|
||||||
|
|
@ -222,41 +220,21 @@ pub const AlphaVantage = struct {
|
||||||
self: *AlphaVantage,
|
self: *AlphaVantage,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError!EtfProfile {
|
) !EtfProfile {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url, &.{
|
const url = try http.buildUrl(allocator, base_url, &.{
|
||||||
.{ "function", "ETF_PROFILE" },
|
.{ "function", "ETF_PROFILE" },
|
||||||
.{ "symbol", symbol },
|
.{ "symbol", symbol },
|
||||||
.{ "apikey", self.api_key },
|
.{ "apikey", self.api_key },
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
defer allocator.free(url);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseEtfProfileResponse(allocator, response.body, symbol);
|
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 --
|
// -- JSON parsing --
|
||||||
|
|
@ -265,17 +243,17 @@ fn parseEtfProfileResponse(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError!EtfProfile {
|
) !EtfProfile {
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
const root = parsed.value.object;
|
||||||
|
|
||||||
// Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit
|
// Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit
|
||||||
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed;
|
if (root.get("Error Message")) |_| return error.RequestFailed;
|
||||||
if (root.get("Note")) |_| return provider.ProviderError.RateLimited;
|
if (root.get("Note")) |_| return error.RateLimited;
|
||||||
if (root.get("Information")) |_| return provider.ProviderError.RateLimited;
|
if (root.get("Information")) |_| return error.RateLimited;
|
||||||
|
|
||||||
var profile = EtfProfile{
|
var profile = EtfProfile{
|
||||||
.symbol = symbol,
|
.symbol = symbol,
|
||||||
|
|
@ -318,13 +296,13 @@ fn parseEtfProfileResponse(
|
||||||
const name = jsonStr(obj.get("sector")) orelse continue;
|
const name = jsonStr(obj.get("sector")) orelse continue;
|
||||||
const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
|
const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
|
||||||
|
|
||||||
const duped_name = allocator.dupe(u8, name) catch return provider.ProviderError.OutOfMemory;
|
const duped_name = try allocator.dupe(u8, name);
|
||||||
sectors.append(allocator, .{
|
try sectors.append(allocator, .{
|
||||||
.name = duped_name,
|
.name = duped_name,
|
||||||
.weight = weight,
|
.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 weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
|
||||||
|
|
||||||
const duped_sym = if (jsonStr(obj.get("symbol"))) |s|
|
const duped_sym = if (jsonStr(obj.get("symbol"))) |s|
|
||||||
(allocator.dupe(u8, s) catch return provider.ProviderError.OutOfMemory)
|
(try allocator.dupe(u8, s))
|
||||||
else
|
else
|
||||||
null;
|
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,
|
.symbol = duped_sym,
|
||||||
.name = duped_name,
|
.name = duped_name,
|
||||||
.weight = weight,
|
.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,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError!CompanyOverview {
|
) !CompanyOverview {
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
const root = parsed.value.object;
|
||||||
|
|
||||||
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed;
|
if (root.get("Error Message")) |_| return error.RequestFailed;
|
||||||
if (root.get("Note")) |_| return provider.ProviderError.RateLimited;
|
if (root.get("Note")) |_| return error.RateLimited;
|
||||||
if (root.get("Information")) |_| return provider.ProviderError.RateLimited;
|
if (root.get("Information")) |_| return error.RateLimited;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.symbol = symbol,
|
.symbol = symbol,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,9 @@ const Date = @import("../models/date.zig").Date;
|
||||||
const OptionContract = @import("../models/option.zig").OptionContract;
|
const OptionContract = @import("../models/option.zig").OptionContract;
|
||||||
const OptionsChain = @import("../models/option.zig").OptionsChain;
|
const OptionsChain = @import("../models/option.zig").OptionsChain;
|
||||||
const ContractType = @import("../models/option.zig").ContractType;
|
const ContractType = @import("../models/option.zig").ContractType;
|
||||||
const provider = @import("provider.zig");
|
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
const optFloat = json_utils.optFloat;
|
const optFloat = json_utils.optFloat;
|
||||||
const optUint = json_utils.optUint;
|
const optUint = json_utils.optUint;
|
||||||
const mapHttpError = json_utils.mapHttpError;
|
|
||||||
|
|
||||||
const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options";
|
const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options";
|
||||||
|
|
||||||
|
|
@ -42,14 +40,14 @@ pub const Cboe = struct {
|
||||||
self: *Cboe,
|
self: *Cboe,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError![]OptionsChain {
|
) ![]OptionsChain {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
// Build URL: {base_url}/{SYMBOL}.json
|
// 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);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseResponse(allocator, response.body, symbol);
|
return parseResponse(allocator, response.body, symbol);
|
||||||
|
|
@ -73,26 +71,26 @@ fn parseResponse(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError![]OptionsChain {
|
) ![]OptionsChain {
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = switch (parsed.value) {
|
const root = switch (parsed.value) {
|
||||||
.object => |o| o,
|
.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,
|
.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 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,
|
.array => |a| a.items,
|
||||||
else => return provider.ProviderError.ParseError,
|
else => return error.ParseError,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse all contracts and group by expiration.
|
// Parse all contracts and group by expiration.
|
||||||
|
|
@ -129,12 +127,11 @@ fn parseResponse(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find or create the expiration bucket
|
// Find or create the expiration bucket
|
||||||
const bucket = exp_map.getOrPut(allocator, occ.expiration) catch
|
const bucket = try exp_map.getOrPut(allocator, occ.expiration);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
|
|
||||||
switch (occ.contract_type) {
|
switch (occ.contract_type) {
|
||||||
.call => bucket.calls.append(allocator, contract) catch return provider.ProviderError.OutOfMemory,
|
.call => try bucket.calls.append(allocator, contract),
|
||||||
.put => bucket.puts.append(allocator, contract) catch return provider.ProviderError.OutOfMemory,
|
.put => try bucket.puts.append(allocator, contract),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +217,7 @@ const ExpMap = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
underlying_price: ?f64,
|
underlying_price: ?f64,
|
||||||
) provider.ProviderError![]OptionsChain {
|
) ![]OptionsChain {
|
||||||
// Sort entries by expiration
|
// Sort entries by expiration
|
||||||
std.mem.sort(Entry, self.entries.items, {}, struct {
|
std.mem.sort(Entry, self.entries.items, {}, struct {
|
||||||
fn lessThan(_: void, a: Entry, b: Entry) bool {
|
fn lessThan(_: void, a: Entry, b: Entry) bool {
|
||||||
|
|
@ -228,8 +225,7 @@ const ExpMap = struct {
|
||||||
}
|
}
|
||||||
}.lessThan);
|
}.lessThan);
|
||||||
|
|
||||||
var chains = allocator.alloc(OptionsChain, self.entries.items.len) catch
|
var chains = try allocator.alloc(OptionsChain, self.entries.items.len);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
|
|
||||||
var initialized: usize = 0;
|
var initialized: usize = 0;
|
||||||
errdefer {
|
errdefer {
|
||||||
|
|
@ -242,14 +238,11 @@ const ExpMap = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (self.entries.items, 0..) |*entry, i| {
|
for (self.entries.items, 0..) |*entry, i| {
|
||||||
const owned_symbol = allocator.dupe(u8, symbol) catch
|
const owned_symbol = try allocator.dupe(u8, symbol);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
errdefer allocator.free(owned_symbol);
|
errdefer allocator.free(owned_symbol);
|
||||||
const calls = entry.calls.toOwnedSlice(allocator) catch
|
const calls = try entry.calls.toOwnedSlice(allocator);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
errdefer allocator.free(calls);
|
errdefer allocator.free(calls);
|
||||||
const puts = entry.puts.toOwnedSlice(allocator) catch
|
const puts = try entry.puts.toOwnedSlice(allocator);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
|
|
||||||
chains[i] = .{
|
chains[i] = .{
|
||||||
.underlying_symbol = owned_symbol,
|
.underlying_symbol = owned_symbol,
|
||||||
|
|
@ -351,5 +344,5 @@ test "parseResponse missing data" {
|
||||||
|
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const result = parseResponse(allocator, body, "AAPL");
|
const result = parseResponse(allocator, body, "AAPL");
|
||||||
try std.testing.expectError(provider.ProviderError.ParseError, result);
|
try std.testing.expectError(error.ParseError, result);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ const RateLimiter = @import("../net/RateLimiter.zig");
|
||||||
const Date = @import("../models/date.zig").Date;
|
const Date = @import("../models/date.zig").Date;
|
||||||
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 json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
const optFloat = json_utils.optFloat;
|
const optFloat = json_utils.optFloat;
|
||||||
const jsonStr = json_utils.jsonStr;
|
const jsonStr = json_utils.jsonStr;
|
||||||
const mapHttpError = json_utils.mapHttpError;
|
|
||||||
|
|
||||||
const base_url = "https://finnhub.io/api/v1";
|
const base_url = "https://finnhub.io/api/v1";
|
||||||
|
|
||||||
|
|
@ -47,7 +45,7 @@ pub const Finnhub = struct {
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
from: ?Date,
|
from: ?Date,
|
||||||
to: ?Date,
|
to: ?Date,
|
||||||
) provider.ProviderError![]EarningsEvent {
|
) ![]EarningsEvent {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
var params: [4][2][]const u8 = undefined;
|
var params: [4][2][]const u8 = undefined;
|
||||||
|
|
@ -70,35 +68,14 @@ pub const Finnhub = struct {
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]) catch
|
const url = try http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(url);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseEarningsResponse(allocator, response.body, symbol);
|
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 --
|
// -- JSON parsing --
|
||||||
|
|
@ -107,24 +84,22 @@ fn parseEarningsResponse(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError![]EarningsEvent {
|
) ![]EarningsEvent {
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
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 cal = root.get("earningsCalendar") orelse {
|
||||||
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(EarningsEvent, 0);
|
||||||
return empty;
|
|
||||||
};
|
};
|
||||||
const items = switch (cal) {
|
const items = switch (cal) {
|
||||||
.array => |a| a.items,
|
.array => |a| a.items,
|
||||||
else => {
|
else => {
|
||||||
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(EarningsEvent, 0);
|
||||||
return empty;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -151,7 +126,7 @@ fn parseEarningsResponse(
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
|
|
||||||
events.append(allocator, .{
|
try events.append(allocator, .{
|
||||||
.symbol = symbol,
|
.symbol = symbol,
|
||||||
.date = date,
|
.date = date,
|
||||||
.estimate = estimate,
|
.estimate = estimate,
|
||||||
|
|
@ -163,10 +138,10 @@ fn parseEarningsResponse(
|
||||||
.revenue_actual = optFloat(obj.get("revenueActual")),
|
.revenue_actual = optFloat(obj.get("revenueActual")),
|
||||||
.revenue_estimate = optFloat(obj.get("revenueEstimate")),
|
.revenue_estimate = optFloat(obj.get("revenueEstimate")),
|
||||||
.report_time = parseReportTime(obj.get("hour")),
|
.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 --
|
// -- Helpers --
|
||||||
|
|
@ -255,7 +230,7 @@ test "parseEarningsResponse error" {
|
||||||
|
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const result = parseEarningsResponse(allocator, body, "AAPL");
|
const result = parseEarningsResponse(allocator, body, "AAPL");
|
||||||
try std.testing.expectError(provider.ProviderError.RequestFailed, result);
|
try std.testing.expectError(error.RequestFailed, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseEarningsResponse empty" {
|
test "parseEarningsResponse empty" {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
//! Shared JSON parsing helpers used by all API providers.
|
//! Shared JSON parsing helpers used by all API providers.
|
||||||
//! Centralises the common patterns: extracting floats, strings,
|
//! 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 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).
|
/// Extract a required float from a JSON value (string, float, or integer).
|
||||||
/// Returns 0 for null, missing, or unparseable values.
|
/// Returns 0 for null, missing, or unparseable values.
|
||||||
|
|
@ -49,13 +47,3 @@ pub fn jsonStr(val: ?std.json.Value) ?[]const u8 {
|
||||||
else => null,
|
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,9 @@ const Candle = @import("../models/candle.zig").Candle;
|
||||||
const Dividend = @import("../models/dividend.zig").Dividend;
|
const Dividend = @import("../models/dividend.zig").Dividend;
|
||||||
const DividendType = @import("../models/dividend.zig").DividendType;
|
const DividendType = @import("../models/dividend.zig").DividendType;
|
||||||
const Split = @import("../models/split.zig").Split;
|
const Split = @import("../models/split.zig").Split;
|
||||||
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 parseJsonFloat = json_utils.parseJsonFloat;
|
||||||
const jsonStr = json_utils.jsonStr;
|
const jsonStr = json_utils.jsonStr;
|
||||||
const mapHttpError = json_utils.mapHttpError;
|
|
||||||
|
|
||||||
const base_url = "https://api.polygon.io";
|
const base_url = "https://api.polygon.io";
|
||||||
|
|
||||||
|
|
@ -48,7 +46,7 @@ pub const Polygon = struct {
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
from: ?Date,
|
from: ?Date,
|
||||||
to: ?Date,
|
to: ?Date,
|
||||||
) provider.ProviderError![]Dividend {
|
) ![]Dividend {
|
||||||
var all_dividends: std.ArrayList(Dividend) = .empty;
|
var all_dividends: std.ArrayList(Dividend) = .empty;
|
||||||
errdefer {
|
errdefer {
|
||||||
for (all_dividends.items) |d| d.deinit(allocator);
|
for (all_dividends.items) |d| d.deinit(allocator);
|
||||||
|
|
@ -84,15 +82,13 @@ pub const Polygon = struct {
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]) catch
|
const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(url);
|
defer allocator.free(url);
|
||||||
|
|
||||||
const authed = appendApiKey(allocator, url, self.api_key) catch
|
const authed = try appendApiKey(allocator, url, self.api_key);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(authed);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
|
next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
|
||||||
|
|
@ -102,18 +98,17 @@ pub const Polygon = struct {
|
||||||
while (next_url) |cursor_url| {
|
while (next_url) |cursor_url| {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
const authed = appendApiKey(allocator, cursor_url, self.api_key) catch
|
const authed = try appendApiKey(allocator, cursor_url, self.api_key);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(authed);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
allocator.free(cursor_url);
|
allocator.free(cursor_url);
|
||||||
next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
|
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.
|
/// Fetch split history for a ticker. Results sorted oldest-first.
|
||||||
|
|
@ -122,21 +117,20 @@ pub const Polygon = struct {
|
||||||
self: *Polygon,
|
self: *Polygon,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError![]Split {
|
) ![]Split {
|
||||||
self.rate_limiter.acquire();
|
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 },
|
.{ "ticker", symbol },
|
||||||
.{ "limit", "1000" },
|
.{ "limit", "1000" },
|
||||||
.{ "sort", "execution_date" },
|
.{ "sort", "execution_date" },
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
defer allocator.free(url);
|
defer allocator.free(url);
|
||||||
|
|
||||||
const authed = appendApiKey(allocator, url, self.api_key) catch
|
const authed = try appendApiKey(allocator, url, self.api_key);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(authed);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseSplitsResponse(allocator, response.body);
|
return parseSplitsResponse(allocator, response.body);
|
||||||
|
|
@ -150,7 +144,7 @@ pub const Polygon = struct {
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date,
|
to: Date,
|
||||||
) provider.ProviderError![]Candle {
|
) ![]Candle {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
var from_buf: [10]u8 = undefined;
|
var from_buf: [10]u8 = undefined;
|
||||||
|
|
@ -159,46 +153,21 @@ pub const Polygon = struct {
|
||||||
const to_str = to.format(&to_buf);
|
const to_str = to.format(&to_buf);
|
||||||
|
|
||||||
// Build URL manually since the path contains the date range
|
// Build URL manually since the path contains the date range
|
||||||
const path = std.fmt.allocPrint(
|
const path = try std.fmt.allocPrint(
|
||||||
allocator,
|
allocator,
|
||||||
"{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000",
|
"{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000",
|
||||||
.{ base_url, symbol, from_str, to_str },
|
.{ base_url, symbol, from_str, to_str },
|
||||||
) catch return provider.ProviderError.OutOfMemory;
|
);
|
||||||
defer allocator.free(path);
|
defer allocator.free(path);
|
||||||
|
|
||||||
const authed = appendApiKey(allocator, path, self.api_key) catch
|
const authed = try appendApiKey(allocator, path, self.api_key);
|
||||||
return provider.ProviderError.OutOfMemory;
|
|
||||||
defer allocator.free(authed);
|
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();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseCandlesResponse(allocator, response.body);
|
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 --
|
// -- JSON parsing --
|
||||||
|
|
@ -207,9 +176,9 @@ fn parseDividendsPage(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
body: []const u8,
|
body: []const u8,
|
||||||
out: *std.ArrayList(Dividend),
|
out: *std.ArrayList(Dividend),
|
||||||
) provider.ProviderError!?[]const u8 {
|
) !?[]const u8 {
|
||||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
const root = parsed.value.object;
|
||||||
|
|
@ -217,7 +186,7 @@ fn parseDividendsPage(
|
||||||
// Check status
|
// Check status
|
||||||
if (root.get("status")) |s| {
|
if (root.get("status")) |s| {
|
||||||
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
|
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;
|
const results = root.get("results") orelse return null;
|
||||||
|
|
@ -244,7 +213,7 @@ fn parseDividendsPage(
|
||||||
const amount = parseJsonFloat(obj.get("cash_amount"));
|
const amount = parseJsonFloat(obj.get("cash_amount"));
|
||||||
if (amount <= 0) continue;
|
if (amount <= 0) continue;
|
||||||
|
|
||||||
out.append(allocator, .{
|
try out.append(allocator, .{
|
||||||
.ex_date = ex_date,
|
.ex_date = ex_date,
|
||||||
.amount = amount,
|
.amount = amount,
|
||||||
.pay_date = parseDateField(obj, "pay_date"),
|
.pay_date = parseDateField(obj, "pay_date"),
|
||||||
|
|
@ -255,7 +224,7 @@ fn parseDividendsPage(
|
||||||
(allocator.dupe(u8, s) catch null)
|
(allocator.dupe(u8, s) catch null)
|
||||||
else
|
else
|
||||||
null,
|
null,
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for next_url (pagination cursor)
|
// Check for next_url (pagination cursor)
|
||||||
|
|
@ -264,34 +233,32 @@ fn parseDividendsPage(
|
||||||
.string => |s| s,
|
.string => |s| s,
|
||||||
else => return null,
|
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 duped;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
const root = parsed.value.object;
|
||||||
|
|
||||||
if (root.get("status")) |s| {
|
if (root.get("status")) |s| {
|
||||||
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
|
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 results = root.get("results") orelse {
|
||||||
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(Split, 0);
|
||||||
return empty;
|
|
||||||
};
|
};
|
||||||
const items = switch (results) {
|
const items = switch (results) {
|
||||||
.array => |a| a.items,
|
.array => |a| a.items,
|
||||||
else => {
|
else => {
|
||||||
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(Split, 0);
|
||||||
return empty;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -313,37 +280,35 @@ fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider.
|
||||||
break :blk Date.parse(s) catch continue;
|
break :blk Date.parse(s) catch continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
splits.append(allocator, .{
|
try splits.append(allocator, .{
|
||||||
.date = date,
|
.date = date,
|
||||||
.numerator = parseJsonFloat(obj.get("split_to")),
|
.numerator = parseJsonFloat(obj.get("split_to")),
|
||||||
.denominator = parseJsonFloat(obj.get("split_from")),
|
.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
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value.object;
|
const root = parsed.value.object;
|
||||||
|
|
||||||
if (root.get("status")) |s| {
|
if (root.get("status")) |s| {
|
||||||
if (s == .string and std.mem.eql(u8, s.string, "ERROR"))
|
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 results = root.get("results") orelse {
|
||||||
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(Candle, 0);
|
||||||
return empty;
|
|
||||||
};
|
};
|
||||||
const items = switch (results) {
|
const items = switch (results) {
|
||||||
.array => |a| a.items,
|
.array => |a| a.items,
|
||||||
else => {
|
else => {
|
||||||
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory;
|
return try allocator.alloc(Candle, 0);
|
||||||
return empty;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -370,7 +335,7 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
|
||||||
|
|
||||||
const close = parseJsonFloat(obj.get("c"));
|
const close = parseJsonFloat(obj.get("c"));
|
||||||
|
|
||||||
candles.append(allocator, .{
|
try candles.append(allocator, .{
|
||||||
.date = date,
|
.date = date,
|
||||||
.open = parseJsonFloat(obj.get("o")),
|
.open = parseJsonFloat(obj.get("o")),
|
||||||
.high = parseJsonFloat(obj.get("h")),
|
.high = parseJsonFloat(obj.get("h")),
|
||||||
|
|
@ -378,10 +343,10 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
|
||||||
.close = close,
|
.close = close,
|
||||||
.adj_close = close, // Polygon adjusted=true gives adjusted values
|
.adj_close = close, // Polygon adjusted=true gives adjusted values
|
||||||
.volume = @intFromFloat(parseJsonFloat(obj.get("v"))),
|
.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 --
|
// -- Helpers --
|
||||||
|
|
@ -506,7 +471,7 @@ test "parseDividendsPage error status" {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = parseDividendsPage(allocator, body, &out);
|
const result = parseDividendsPage(allocator, body, &out);
|
||||||
try std.testing.expectError(provider.ProviderError.RequestFailed, result);
|
try std.testing.expectError(error.RequestFailed, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseSplitsResponse basic" {
|
test "parseSplitsResponse basic" {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -12,7 +12,6 @@ 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 Candle = @import("../models/candle.zig").Candle;
|
const Candle = @import("../models/candle.zig").Candle;
|
||||||
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 parseJsonFloat = json_utils.parseJsonFloat;
|
||||||
|
|
||||||
|
|
@ -45,7 +44,7 @@ pub const TwelveData = struct {
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date,
|
to: Date,
|
||||||
) provider.ProviderError![]Candle {
|
) ![]Candle {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
var from_buf: [10]u8 = undefined;
|
var from_buf: [10]u8 = undefined;
|
||||||
|
|
@ -53,22 +52,17 @@ pub const TwelveData = struct {
|
||||||
const from_str = from.format(&from_buf);
|
const from_str = from.format(&from_buf);
|
||||||
const to_str = to.format(&to_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 },
|
.{ "symbol", symbol },
|
||||||
.{ "interval", "1day" },
|
.{ "interval", "1day" },
|
||||||
.{ "start_date", from_str },
|
.{ "start_date", from_str },
|
||||||
.{ "end_date", to_str },
|
.{ "end_date", to_str },
|
||||||
.{ "outputsize", "5000" },
|
.{ "outputsize", "5000" },
|
||||||
.{ "apikey", self.api_key },
|
.{ "apikey", self.api_key },
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
defer allocator.free(url);
|
defer allocator.free(url);
|
||||||
|
|
||||||
var response = self.client.get(url) catch |err| return switch (err) {
|
var response = try self.client.get(url);
|
||||||
error.RateLimited => provider.ProviderError.RateLimited,
|
|
||||||
error.Unauthorized => provider.ProviderError.Unauthorized,
|
|
||||||
error.NotFound => provider.ProviderError.NotFound,
|
|
||||||
else => provider.ProviderError.RequestFailed,
|
|
||||||
};
|
|
||||||
defer response.deinit();
|
defer response.deinit();
|
||||||
|
|
||||||
return parseTimeSeriesResponse(allocator, response.body);
|
return parseTimeSeriesResponse(allocator, response.body);
|
||||||
|
|
@ -84,7 +78,7 @@ pub const TwelveData = struct {
|
||||||
|
|
||||||
/// Parse and print quote data. Caller should use this within the
|
/// Parse and print quote data. Caller should use this within the
|
||||||
/// lifetime of the QuoteResponse.
|
/// 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);
|
return parseQuoteBody(allocator, self.body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -160,21 +154,16 @@ pub const TwelveData = struct {
|
||||||
self: *TwelveData,
|
self: *TwelveData,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
symbol: []const u8,
|
symbol: []const u8,
|
||||||
) provider.ProviderError!QuoteResponse {
|
) !QuoteResponse {
|
||||||
self.rate_limiter.acquire();
|
self.rate_limiter.acquire();
|
||||||
|
|
||||||
const url = http.buildUrl(allocator, base_url ++ "/quote", &.{
|
const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{
|
||||||
.{ "symbol", symbol },
|
.{ "symbol", symbol },
|
||||||
.{ "apikey", self.api_key },
|
.{ "apikey", self.api_key },
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
defer allocator.free(url);
|
defer allocator.free(url);
|
||||||
|
|
||||||
var response = self.client.get(url) catch |err| return switch (err) {
|
var response = try self.client.get(url);
|
||||||
error.RateLimited => provider.ProviderError.RateLimited,
|
|
||||||
error.Unauthorized => provider.ProviderError.Unauthorized,
|
|
||||||
error.NotFound => provider.ProviderError.NotFound,
|
|
||||||
else => provider.ProviderError.RequestFailed,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transfer ownership of body to QuoteResponse
|
// Transfer ownership of body to QuoteResponse
|
||||||
const body = response.body;
|
const body = response.body;
|
||||||
|
|
@ -185,35 +174,13 @@ pub const TwelveData = struct {
|
||||||
.allocator = allocator,
|
.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 --
|
// -- 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
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
||||||
const root = parsed.value;
|
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")) {
|
if (std.mem.eql(u8, status.string, "error")) {
|
||||||
// Check error code
|
// Check error code
|
||||||
if (root.object.get("code")) |code| {
|
if (root.object.get("code")) |code| {
|
||||||
if (code == .integer and code.integer == 429) return provider.ProviderError.RateLimited;
|
if (code == .integer and code.integer == 429) return error.RateLimited;
|
||||||
if (code == .integer and code.integer == 401) return provider.ProviderError.Unauthorized;
|
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) {
|
const values = switch (values_json) {
|
||||||
.array => |a| a.items,
|
.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.
|
// 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;
|
break :blk Date.parse(date_part) catch continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
candles.append(allocator, .{
|
try candles.append(allocator, .{
|
||||||
.date = date,
|
.date = date,
|
||||||
.open = parseJsonFloat(obj.get("open")),
|
.open = parseJsonFloat(obj.get("open")),
|
||||||
.high = parseJsonFloat(obj.get("high")),
|
.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
|
// Twelve Data close is split-adjusted only, not dividend-adjusted
|
||||||
.adj_close = parseJsonFloat(obj.get("close")),
|
.adj_close = parseJsonFloat(obj.get("close")),
|
||||||
.volume = @intFromFloat(parseJsonFloat(obj.get("volume"))),
|
.volume = @intFromFloat(parseJsonFloat(obj.get("volume"))),
|
||||||
}) catch return provider.ProviderError.OutOfMemory;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse to get oldest-first ordering
|
// 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);
|
std.mem.reverse(Candle, slice);
|
||||||
return 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
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return provider.ProviderError.ParseError;
|
return error.ParseError;
|
||||||
|
|
||||||
// Check for error
|
// Check for error
|
||||||
if (parsed.value.object.get("status")) |status| {
|
if (parsed.value.object.get("status")) |status| {
|
||||||
if (status == .string and std.mem.eql(u8, status.string, "error")) {
|
if (status == .string and std.mem.eql(u8, status.string, "error")) {
|
||||||
var p = parsed;
|
var p = parsed;
|
||||||
p.deinit();
|
p.deinit();
|
||||||
return provider.ProviderError.RequestFailed;
|
return error.RequestFailed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue