snap performance for weekly returns
This commit is contained in:
parent
efb4d57c23
commit
3f9cb33be0
1 changed files with 46 additions and 1 deletions
|
|
@ -487,15 +487,33 @@ pub fn formatReturn(buf: []u8, value: f64) []const u8 {
|
||||||
|
|
||||||
/// Compute 1-week return from candle data: (latest_close / close_7_days_ago) - 1.
|
/// Compute 1-week return from candle data: (latest_close / close_7_days_ago) - 1.
|
||||||
/// Candles must be sorted by date ascending.
|
/// Candles must be sorted by date ascending.
|
||||||
|
///
|
||||||
|
/// Bounded by `max_snap_days`, same as `findNearestCandle`. The backward scan used
|
||||||
|
/// to be unbounded with a comment asserting "at most ~10 steps for daily candles"
|
||||||
|
/// - an assumption about the data, not a constraint on the code. A hole in the
|
||||||
|
/// candle cache therefore turned this into a month's return still labelled "Week",
|
||||||
|
/// silently and with no way to tell from the output. That is exactly the failure
|
||||||
|
/// `max_snap_days` was introduced to prevent ("Beyond this, the data is likely
|
||||||
|
/// missing and the result would be misleading"); this function simply never
|
||||||
|
/// applied it.
|
||||||
|
///
|
||||||
|
/// Real caches do have holes: a snapshot taken 2026-08-29 carried
|
||||||
|
/// `stale_count:num:3`, with three symbols a full day behind their peers.
|
||||||
|
///
|
||||||
|
/// Returns null rather than a wrong number, so the caller renders "--" and the
|
||||||
|
/// gap is visible instead of plausible.
|
||||||
pub fn weekReturn(candles: []const Candle) ?f64 {
|
pub fn weekReturn(candles: []const Candle) ?f64 {
|
||||||
if (candles.len < 2) return null;
|
if (candles.len < 2) return null;
|
||||||
const latest = candles[candles.len - 1];
|
const latest = candles[candles.len - 1];
|
||||||
const target_date = latest.date.addDays(-7);
|
const target_date = latest.date.addDays(-7);
|
||||||
|
|
||||||
// Linear scan backward (at most ~10 steps for daily candles)
|
|
||||||
var i: usize = candles.len - 2;
|
var i: usize = candles.len - 2;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (candles[i].date.days <= target_date.days) {
|
if (candles[i].date.days <= target_date.days) {
|
||||||
|
// How far BEFORE the target we landed. A weekend or holiday puts the
|
||||||
|
// nearest bar a few days early, which is normal and fine; a cache hole
|
||||||
|
// puts it weeks early, which is not a week's return.
|
||||||
|
if (target_date.days - candles[i].date.days > max_snap_days) return null;
|
||||||
if (candles[i].close == 0) return null;
|
if (candles[i].close == 0) return null;
|
||||||
return (latest.close / candles[i].close) - 1.0;
|
return (latest.close / candles[i].close) - 1.0;
|
||||||
}
|
}
|
||||||
|
|
@ -543,6 +561,33 @@ test "weekReturn snaps to nearest trading day" {
|
||||||
try std.testing.expectApproxEqAbs(@as(f64, 0.1667), r, 0.001);
|
try std.testing.expectApproxEqAbs(@as(f64, 0.1667), r, 0.001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "weekReturn refuses a gap wider than max_snap_days" {
|
||||||
|
// A hole in the candle cache. Latest is Feb 10; 7 days back is Feb 3, but the
|
||||||
|
// previous bar is Jan 1 - forty days earlier. The old unbounded scan happily
|
||||||
|
// returned (105/90 - 1) = +16.67% and labelled it "Week".
|
||||||
|
const holed = [_]Candle{
|
||||||
|
.{ .date = Date.fromYmd(2024, 1, 1), .open = 90, .high = 90, .low = 90, .close = 90, .adj_close = 90, .volume = 0 },
|
||||||
|
.{ .date = Date.fromYmd(2024, 2, 10), .open = 105, .high = 105, .low = 105, .close = 105, .adj_close = 105, .volume = 0 },
|
||||||
|
};
|
||||||
|
try std.testing.expect(weekReturn(&holed) == null);
|
||||||
|
|
||||||
|
// The boundary is inclusive and generous enough for a holiday week: landing
|
||||||
|
// exactly `max_snap_days` before the target still counts. Target is Jan 21 - 7
|
||||||
|
// = Jan 14; Jan 4 is 10 days before that.
|
||||||
|
const at_limit = [_]Candle{
|
||||||
|
.{ .date = Date.fromYmd(2024, 1, 4), .open = 90, .high = 90, .low = 90, .close = 90, .adj_close = 90, .volume = 0 },
|
||||||
|
.{ .date = Date.fromYmd(2024, 1, 21), .open = 99, .high = 99, .low = 99, .close = 99, .adj_close = 99, .volume = 0 },
|
||||||
|
};
|
||||||
|
try std.testing.expectApproxEqAbs(@as(f64, 0.10), weekReturn(&at_limit).?, 0.001);
|
||||||
|
|
||||||
|
// One day past it is refused, so the cutoff is actually enforced rather than
|
||||||
|
// merely present.
|
||||||
|
const past_limit = [_]Candle{
|
||||||
|
.{ .date = Date.fromYmd(2024, 1, 3), .open = 90, .high = 90, .low = 90, .close = 90, .adj_close = 90, .volume = 0 },
|
||||||
|
.{ .date = Date.fromYmd(2024, 1, 21), .open = 99, .high = 99, .low = 99, .close = 99, .adj_close = 99, .volume = 0 },
|
||||||
|
};
|
||||||
|
try std.testing.expect(weekReturn(&past_limit) == null);
|
||||||
|
}
|
||||||
test "weekReturn zero close returns null" {
|
test "weekReturn zero close returns null" {
|
||||||
const candles = [_]Candle{
|
const candles = [_]Candle{
|
||||||
.{ .date = Date.fromYmd(2024, 1, 1), .open = 0, .high = 0, .low = 0, .close = 0, .adj_close = 0, .volume = 0 },
|
.{ .date = Date.fromYmd(2024, 1, 1), .open = 0, .high = 0, .low = 0, .close = 0, .adj_close = 0, .volume = 0 },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue