fix fetchQuote idiocy
This commit is contained in:
parent
0f09ef5cff
commit
373c30d947
2 changed files with 51 additions and 146 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue