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 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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue