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

View file

@ -325,31 +325,8 @@ pub const DataService = struct {
/// No cache -- always fetches fresh from TwelveData. /// No cache -- always fetches fresh from TwelveData.
pub fn getQuote(self: *DataService, symbol: []const u8) DataError!Quote { pub fn getQuote(self: *DataService, symbol: []const u8) DataError!Quote {
var td = try self.getTwelveData(); var td = try self.getTwelveData();
var resp = td.fetchQuote(self.allocator, symbol) catch return td.fetchQuote(self.allocator, symbol) catch
return DataError.FetchFailed; 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. /// Fetch company overview (sector, industry, country, market cap) from Alpha Vantage.