90 lines
3.7 KiB
Zig
90 lines
3.7 KiB
Zig
const Date = @import("date.zig").Date;
|
|
|
|
pub const ReportTime = enum {
|
|
bmo, // before market open
|
|
amc, // after market close
|
|
dmh, // during market hours
|
|
unknown,
|
|
};
|
|
|
|
/// An earnings event (historical or upcoming).
|
|
pub const EarningsEvent = struct {
|
|
symbol: []const u8 = "",
|
|
date: Date,
|
|
/// Estimated EPS (analyst consensus)
|
|
estimate: ?f64 = null,
|
|
/// Actual reported EPS (null if upcoming)
|
|
actual: ?f64 = null,
|
|
/// Surprise amount (actual - estimate)
|
|
surprise: ?f64 = null,
|
|
/// Surprise percentage
|
|
surprise_percent: ?f64 = null,
|
|
/// Fiscal quarter (1-4)
|
|
quarter: ?u8 = null,
|
|
/// Fiscal year
|
|
fiscal_year: ?i16 = null,
|
|
/// Revenue actual
|
|
revenue_actual: ?f64 = null,
|
|
/// Revenue estimate
|
|
revenue_estimate: ?f64 = null,
|
|
/// When earnings are reported relative to market hours
|
|
report_time: ReportTime = .unknown,
|
|
|
|
pub fn isFuture(self: EarningsEvent) bool {
|
|
return self.actual == null;
|
|
}
|
|
|
|
pub fn surpriseAmount(self: EarningsEvent) ?f64 {
|
|
if (self.surprise) |s| return s;
|
|
const act = self.actual orelse return null;
|
|
const est = self.estimate orelse return null;
|
|
return act - est;
|
|
}
|
|
|
|
pub fn surprisePct(self: EarningsEvent) ?f64 {
|
|
if (self.surprise_percent) |s| return s;
|
|
const act = self.actual orelse return null;
|
|
const est = self.estimate orelse return null;
|
|
if (est == 0) return null;
|
|
return ((act - est) / @abs(est)) * 100.0;
|
|
}
|
|
};
|
|
|
|
const std = @import("std");
|
|
|
|
test "isFuture" {
|
|
const future = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 20000 }, .estimate = 1.5 };
|
|
try std.testing.expect(future.isFuture());
|
|
const past = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.6, .estimate = 1.5 };
|
|
try std.testing.expect(!past.isFuture());
|
|
}
|
|
|
|
test "surpriseAmount" {
|
|
// With surprise field set directly
|
|
const with_surprise = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .surprise = 0.15 };
|
|
try std.testing.expectApproxEqAbs(@as(f64, 0.15), with_surprise.surpriseAmount().?, 0.001);
|
|
// With actual and estimate (computed)
|
|
const computed = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65, .estimate = 1.50 };
|
|
try std.testing.expectApproxEqAbs(@as(f64, 0.15), computed.surpriseAmount().?, 0.001);
|
|
// Missing actual -> null
|
|
const no_actual = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .estimate = 1.50 };
|
|
try std.testing.expect(no_actual.surpriseAmount() == null);
|
|
// Missing estimate -> null
|
|
const no_estimate = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65 };
|
|
try std.testing.expect(no_estimate.surpriseAmount() == null);
|
|
}
|
|
|
|
test "surprisePct" {
|
|
// With surprise_percent set directly
|
|
const with_pct = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .surprise_percent = 10.0 };
|
|
try std.testing.expectApproxEqAbs(@as(f64, 10.0), with_pct.surprisePct().?, 0.001);
|
|
// Computed: actual=1.65, estimate=1.50 -> (0.15/1.50)*100 = 10%
|
|
const computed = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65, .estimate = 1.50 };
|
|
try std.testing.expectApproxEqAbs(@as(f64, 10.0), computed.surprisePct().?, 0.001);
|
|
// Estimate = 0 -> null (division by zero guard)
|
|
const zero_est = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.0, .estimate = 0.0 };
|
|
try std.testing.expect(zero_est.surprisePct() == null);
|
|
// Negative surprise: actual < estimate
|
|
const miss = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.35, .estimate = 1.50 };
|
|
try std.testing.expect(miss.surprisePct().? < 0);
|
|
}
|