zfin/src/models/earnings.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);
}