expose candle freshness data to downstream
This commit is contained in:
parent
75252c5beb
commit
3cedde20eb
2 changed files with 96 additions and 22 deletions
111
src/market.zig
111
src/market.zig
|
|
@ -275,33 +275,60 @@ fn latestAvailability(now_s: i64, kind: InstrumentKind) Availability {
|
|||
};
|
||||
}
|
||||
|
||||
/// Expiry to stamp on candle meta after an *incremental* fetch on a stale
|
||||
/// entry returned zero new bars. Three cases:
|
||||
/// Classification of candle-cache freshness; see `candleFreshness`.
|
||||
pub const CandleFreshness = enum {
|
||||
/// The cache already holds the latest available bar, or nothing newer
|
||||
/// than it is due yet (a non-trading-day weekend/holiday gap).
|
||||
current,
|
||||
/// A newer session's bar is due but not yet posted by the provider,
|
||||
/// still within the provider-lag grace window - worth retrying soon.
|
||||
lagging,
|
||||
/// A newer bar has been overdue for the full grace window: almost
|
||||
/// certainly an un-modeled closure or ad-hoc halt, not mere lag, so
|
||||
/// retrying is pointless until the next real session.
|
||||
overdue,
|
||||
};
|
||||
|
||||
/// Candle-cache freshness for `kind` as of `now_s`, given `last_cached`
|
||||
/// (the newest bar date already in the cache). Names the three cases an
|
||||
/// incremental fetch returning zero new bars can be in, so callers (e.g.
|
||||
/// a refresh cron) can react to provider lag without re-deriving the
|
||||
/// market calendar:
|
||||
///
|
||||
/// 1. **Nothing newer is due** - a genuine non-trading-day gap
|
||||
/// (weekend/holiday) or the cache is already caught up: use the
|
||||
/// normal next-boundary expiry.
|
||||
/// 2. **Newer data is due, still within the provider-lag grace
|
||||
/// window** (`provider_lag_grace_s`): the provider just hasn't
|
||||
/// posted the just-closed bar yet, so retry soon (`short_retry_s`).
|
||||
/// 3. **Newer data has been due for the full grace window**
|
||||
/// (`provider_lag_grace_s`): the calendar's "trading day" was almost
|
||||
/// certainly an un-modeled closure (e.g. Good Friday) or ad-hoc halt
|
||||
/// - the bar is never going to appear. Stop the short-retry thrash
|
||||
/// and fall back to the normal next-boundary expiry, so we wait for
|
||||
/// the next real session instead of refetching every `short_retry_s`
|
||||
/// all evening (and, for a Friday closure, all weekend).
|
||||
/// 1. `.current` - **nothing newer is due**: a genuine non-trading-day
|
||||
/// gap (weekend/holiday) or the cache is already caught up.
|
||||
/// 2. `.lagging` - **newer data is due, still within the provider-lag
|
||||
/// grace window** (`provider_lag_grace_s`): the provider just hasn't
|
||||
/// posted the just-closed bar yet.
|
||||
/// 3. `.overdue` - **newer data has been due for the full grace
|
||||
/// window**: the calendar's "trading day" was almost certainly an
|
||||
/// un-modeled closure (e.g. Good Friday) or ad-hoc halt - the bar is
|
||||
/// never going to appear.
|
||||
///
|
||||
/// Pure given `now_s`, so it is fully deterministic for tests.
|
||||
pub fn candleFreshness(now_s: i64, kind: InstrumentKind, last_cached: Date) CandleFreshness {
|
||||
const avail = latestAvailability(now_s, kind);
|
||||
if (!last_cached.lessThan(avail.data_date)) return .current;
|
||||
if (now_s - avail.instant_s < provider_lag_grace_s) return .lagging;
|
||||
return .overdue;
|
||||
}
|
||||
|
||||
/// Expiry to stamp on candle meta after an *incremental* fetch on a
|
||||
/// stale entry returned zero new bars. Maps `candleFreshness` to a
|
||||
/// boundary: a `.lagging` bar retries soon (`short_retry_s`); `.current`
|
||||
/// and `.overdue` both fall back to the normal next-boundary expiry.
|
||||
/// Routing `.overdue` to the long boundary stops the short-retry thrash
|
||||
/// so an un-modeled closure (e.g. Good Friday) waits for the next real
|
||||
/// session instead of refetching every `short_retry_s` all evening (and,
|
||||
/// for a Friday closure, all weekend).
|
||||
///
|
||||
/// `last_cached` is the newest date already in the candle cache. Pure
|
||||
/// given `now_s`, so it is fully deterministic for tests.
|
||||
pub fn staleCandleExpiry(now_s: i64, kind: InstrumentKind, last_cached: Date) i64 {
|
||||
const avail = latestAvailability(now_s, kind);
|
||||
// Case 1: nothing newer than the cache is due yet.
|
||||
if (!last_cached.lessThan(avail.data_date)) return nextCandleExpiry(now_s, kind);
|
||||
// Case 2: due data is merely late (still inside the grace window) -> short retry.
|
||||
if (now_s - avail.instant_s < provider_lag_grace_s) return now_s + short_retry_s;
|
||||
// Case 3: overdue for the whole grace window -> assume closure, wait for next boundary.
|
||||
return nextCandleExpiry(now_s, kind);
|
||||
return switch (candleFreshness(now_s, kind, last_cached)) {
|
||||
.current, .overdue => nextCandleExpiry(now_s, kind),
|
||||
.lagging => now_s + short_retry_s,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert an ET local wall-clock instant (`d` at `tod_s` seconds since
|
||||
|
|
@ -536,3 +563,43 @@ test "staleCandleExpiry mutual_fund: late NAV within grace retries soon" {
|
|||
// Cache already has Monday -> nothing newer is due -> normal boundary.
|
||||
try testing.expectEqual(nextCandleExpiry(now, .mutual_fund), staleCandleExpiry(now, .mutual_fund, Date.fromYmd(2025, 6, 16)));
|
||||
}
|
||||
|
||||
test "candleFreshness equity: lagging vs current" {
|
||||
// Thu 2025-06-12, 17:30 ET: Thursday's bar is due (16:55 passed) but
|
||||
// only ~35m overdue, well inside the lag grace window.
|
||||
const now = etLocalToUtc(Date.fromYmd(2025, 6, 12), 17 * std.time.s_per_hour + 30 * std.time.s_per_min);
|
||||
// Cache last has Wed -> Thursday's bar is due but unposted -> lagging.
|
||||
try testing.expectEqual(CandleFreshness.lagging, candleFreshness(now, .equity, Date.fromYmd(2025, 6, 11)));
|
||||
// Cache already has Thu -> nothing newer is due -> current.
|
||||
try testing.expectEqual(CandleFreshness.current, candleFreshness(now, .equity, Date.fromYmd(2025, 6, 12)));
|
||||
}
|
||||
|
||||
test "candleFreshness equity: weekend gap is current" {
|
||||
// Sat 2025-06-14, 10:00 ET: latest available equity data is still
|
||||
// Friday, which the cache already has - nothing is overdue.
|
||||
const now = etLocalToUtc(Date.fromYmd(2025, 6, 14), 10 * std.time.s_per_hour);
|
||||
try testing.expectEqual(CandleFreshness.current, candleFreshness(now, .equity, Date.fromYmd(2025, 6, 13)));
|
||||
}
|
||||
|
||||
test "candleFreshness equity: un-modeled closure goes overdue after grace" {
|
||||
// Good Friday 2025-04-18 is NOT a modeled holiday (see isHoliday), so
|
||||
// the calendar treats it as a trading day and the bar never appears.
|
||||
const last_cached = Date.fromYmd(2025, 4, 17); // Thursday
|
||||
// Just inside the window (+89m): still lagging in case the bar is late.
|
||||
const inside = etLocalToUtc(Date.fromYmd(2025, 4, 18), 18 * std.time.s_per_hour + 24 * std.time.s_per_min);
|
||||
try testing.expectEqual(CandleFreshness.lagging, candleFreshness(inside, .equity, last_cached));
|
||||
// At the 90-minute window: conclude closure -> overdue.
|
||||
const at_window = etLocalToUtc(Date.fromYmd(2025, 4, 18), 18 * std.time.s_per_hour + 25 * std.time.s_per_min);
|
||||
try testing.expectEqual(CandleFreshness.overdue, candleFreshness(at_window, .equity, last_cached));
|
||||
}
|
||||
|
||||
test "candleFreshness mutual_fund: late NAV is lagging" {
|
||||
// Tue 2025-06-17, 04:30 ET: Monday's NAV (data_date 2025-06-16) is due
|
||||
// (posts ~03:25 ET) but the provider is lagging ~65m, inside the grace
|
||||
// window.
|
||||
const now = etLocalToUtc(Date.fromYmd(2025, 6, 17), 4 * std.time.s_per_hour + 30 * std.time.s_per_min);
|
||||
// Cache has through Friday's NAV -> Monday's is missing -> lagging.
|
||||
try testing.expectEqual(CandleFreshness.lagging, candleFreshness(now, .mutual_fund, Date.fromYmd(2025, 6, 13)));
|
||||
// Cache already has Monday -> nothing newer is due -> current.
|
||||
try testing.expectEqual(CandleFreshness.current, candleFreshness(now, .mutual_fund, Date.fromYmd(2025, 6, 16)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,13 @@ pub const indicators = @import("analytics/indicators.zig");
|
|||
/// Fundamental analysis: valuation, momentum, quality, and yield scoring.
|
||||
pub const analysis = @import("analytics/analysis.zig");
|
||||
|
||||
// ── Market calendar ──────────────────────────────────────────
|
||||
|
||||
/// Trading-day calendar (NYSE holidays, weekends) and market-aware
|
||||
/// candle-freshness boundaries: `classify`, `candleFreshness`,
|
||||
/// `nextCandleExpiry`, `staleCandleExpiry`.
|
||||
pub const market = @import("market.zig");
|
||||
|
||||
// ── Classification ───────────────────────────────────────────
|
||||
|
||||
/// Sector/industry/country classification for enriched securities.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue