fix fetchQuote idiocy

This commit is contained in:
Emil Lerch 2026-03-06 16:35:57 -08:00
parent 0f09ef5cff
commit 373c30d947
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 51 additions and 146 deletions

View file

@ -12,6 +12,7 @@ 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 Quote = @import("../models/quote.zig").Quote;
const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat;
@ -68,93 +69,11 @@ pub const TwelveData = struct {
return parseTimeSeriesResponse(allocator, response.body);
}
pub const QuoteResponse = struct {
body: []const u8,
allocator: std.mem.Allocator,
pub fn deinit(self: *QuoteResponse) void {
self.allocator.free(self.body);
}
/// Parse and print quote data. Caller should use this within the
/// lifetime of the QuoteResponse.
pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) !ParsedQuote {
return parseQuoteBody(allocator, self.body);
}
};
pub const ParsedQuote = struct {
parsed: std.json.Parsed(std.json.Value),
pub fn deinit(self: *ParsedQuote) void {
self.parsed.deinit();
}
fn root(self: ParsedQuote) std.json.ObjectMap {
return self.parsed.value.object;
}
pub fn symbol(self: ParsedQuote) []const u8 {
return jsonStr(self.root().get("symbol"));
}
pub fn name(self: ParsedQuote) []const u8 {
return jsonStr(self.root().get("name"));
}
pub fn exchange(self: ParsedQuote) []const u8 {
return jsonStr(self.root().get("exchange"));
}
pub fn datetime(self: ParsedQuote) []const u8 {
return jsonStr(self.root().get("datetime"));
}
pub fn close(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("close"));
}
pub fn open(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("open"));
}
pub fn high(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("high"));
}
pub fn low(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("low"));
}
pub fn volume(self: ParsedQuote) u64 {
return @intFromFloat(parseJsonFloat(self.root().get("volume")));
}
pub fn previous_close(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("previous_close"));
}
pub fn change(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("change"));
}
pub fn percent_change(self: ParsedQuote) f64 {
return parseJsonFloat(self.root().get("percent_change"));
}
pub fn average_volume(self: ParsedQuote) u64 {
return @intFromFloat(parseJsonFloat(self.root().get("average_volume")));
}
pub fn fifty_two_week_low(self: ParsedQuote) f64 {
const ftw = self.root().get("fifty_two_week") orelse return 0;
return switch (ftw) {
.object => |o| parseJsonFloat(o.get("low")),
else => 0,
};
}
pub fn fifty_two_week_high(self: ParsedQuote) f64 {
const ftw = self.root().get("fifty_two_week") orelse return 0;
return switch (ftw) {
.object => |o| parseJsonFloat(o.get("high")),
else => 0,
};
}
};
pub fn fetchQuote(
self: *TwelveData,
allocator: std.mem.Allocator,
symbol: []const u8,
) !QuoteResponse {
) !Quote {
self.rate_limiter.acquire();
const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{
@ -164,15 +83,9 @@ pub const TwelveData = struct {
defer allocator.free(url);
var response = try self.client.get(url);
defer response.deinit();
// Transfer ownership of body to QuoteResponse
const body = response.body;
response.body = &.{};
return .{
.body = body,
.allocator = allocator,
};
return parseQuoteResponse(allocator, response.body, symbol);
}
};
@ -244,28 +157,47 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Ca
return slice;
}
fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) !TwelveData.ParsedQuote {
fn parseQuoteResponse(allocator: std.mem.Allocator, body: []const u8, symbol: []const u8) !Quote {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return error.ParseError;
defer parsed.deinit();
const root = parsed.value.object;
// Check for error
if (parsed.value.object.get("status")) |status| {
if (root.get("status")) |status| {
if (status == .string and std.mem.eql(u8, status.string, "error")) {
var p = parsed;
p.deinit();
if (root.get("code")) |code| {
if (code == .integer and code.integer == 429) return error.RateLimited;
}
return error.RequestFailed;
}
}
return .{ .parsed = parsed };
}
const ftw = root.get("fifty_two_week");
/// TwelveData-specific: returns empty string instead of null for quote display convenience.
fn jsonStr(val: ?std.json.Value) []const u8 {
const v = val orelse return "";
return switch (v) {
.string => |s| s,
else => "",
return .{
.symbol = symbol,
.name = symbol,
.exchange = "",
.datetime = "",
.close = parseJsonFloat(root.get("close")),
.open = parseJsonFloat(root.get("open")),
.high = parseJsonFloat(root.get("high")),
.low = parseJsonFloat(root.get("low")),
.volume = @intFromFloat(parseJsonFloat(root.get("volume"))),
.previous_close = parseJsonFloat(root.get("previous_close")),
.change = parseJsonFloat(root.get("change")),
.percent_change = parseJsonFloat(root.get("percent_change")),
.average_volume = @intFromFloat(parseJsonFloat(root.get("average_volume"))),
.fifty_two_week_low = if (ftw) |f| switch (f) {
.object => |o| parseJsonFloat(o.get("low")),
else => 0,
} else 0,
.fifty_two_week_high = if (ftw) |f| switch (f) {
.object => |o| parseJsonFloat(o.get("high")),
else => 0,
} else 0,
};
}
@ -371,7 +303,7 @@ test "parseTimeSeriesResponse empty values" {
try std.testing.expectEqual(@as(usize, 0), candles.len);
}
test "parseQuoteBody basic" {
test "parseQuoteResponse basic" {
const body =
\\{
\\ "symbol": "AAPL",
@ -395,26 +327,22 @@ test "parseQuoteBody basic" {
;
const allocator = std.testing.allocator;
var quote = try parseQuoteBody(allocator, body);
defer quote.deinit();
const quote = try parseQuoteResponse(allocator, body, "AAPL");
try std.testing.expectEqualStrings("AAPL", quote.symbol());
try std.testing.expectEqualStrings("Apple Inc", quote.name());
try std.testing.expectEqualStrings("NASDAQ", quote.exchange());
try std.testing.expectApproxEqAbs(@as(f64, 183.63), quote.close(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 182.15), quote.open(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 185.0), quote.high(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 181.5), quote.low(), 0.01);
try std.testing.expectEqual(@as(u64, 65000000), quote.volume());
try std.testing.expectApproxEqAbs(@as(f64, 181.18), quote.previous_close(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 2.45), quote.change(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 1.35), quote.percent_change(), 0.01);
try std.testing.expectEqual(@as(u64, 55000000), quote.average_volume());
try std.testing.expectApproxEqAbs(@as(f64, 140.0), quote.fifty_two_week_low(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 200.0), quote.fifty_two_week_high(), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 183.63), quote.close, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 182.15), quote.open, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 185.0), quote.high, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 181.5), quote.low, 0.01);
try std.testing.expectEqual(@as(u64, 65000000), quote.volume);
try std.testing.expectApproxEqAbs(@as(f64, 181.18), quote.previous_close, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 2.45), quote.change, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 1.35), quote.percent_change, 0.01);
try std.testing.expectEqual(@as(u64, 55000000), quote.average_volume);
try std.testing.expectApproxEqAbs(@as(f64, 140.0), quote.fifty_two_week_low, 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 200.0), quote.fifty_two_week_high, 0.01);
}
test "parseQuoteBody error response" {
test "parseQuoteResponse error response" {
const body =
\\{
\\ "status": "error",
@ -424,7 +352,7 @@ test "parseQuoteBody error response" {
;
const allocator = std.testing.allocator;
const result = parseQuoteBody(allocator, body);
const result = parseQuoteResponse(allocator, body, "BAD");
try std.testing.expectError(error.RequestFailed, result);
}

View file

@ -325,31 +325,8 @@ pub const DataService = struct {
/// No cache -- always fetches fresh from TwelveData.
pub fn getQuote(self: *DataService, symbol: []const u8) DataError!Quote {
var td = try self.getTwelveData();
var resp = td.fetchQuote(self.allocator, symbol) catch
return td.fetchQuote(self.allocator, symbol) catch
return DataError.FetchFailed;
defer resp.deinit();
var parsed = resp.parse(self.allocator) catch
return DataError.ParseError;
defer parsed.deinit();
return .{
.symbol = symbol,
.name = symbol, // name is in parsed JSON but lifetime is tricky; use symbol
.exchange = "",
.datetime = "",
.close = parsed.close(),
.open = parsed.open(),
.high = parsed.high(),
.low = parsed.low(),
.volume = parsed.volume(),
.previous_close = parsed.previous_close(),
.change = parsed.change(),
.percent_change = parsed.percent_change(),
.average_volume = parsed.average_volume(),
.fifty_two_week_low = parsed.fifty_two_week_low(),
.fifty_two_week_high = parsed.fifty_two_week_high(),
};
}
/// Fetch company overview (sector, industry, country, market cap) from Alpha Vantage.