Weekend Oracle Guard
Verified against: F-342 (2026-11-10, NYSE 2026–2027 calendar), F-350 (2026-11-10) · spec docs/PRD.md §4.7 · docs/work/interface-v7.md §A2 · D-018 (2026-11-10) · tests listed below (evidence/wo-21/cargo-test.log, 2026-11-10) · testnet: docs/EVIDENCE.md §G2b (2026-11-10T16:52Z, 91db9ea)
Status: live on testnet (EVIDENCE §G2b, 2026-11-10T16:52Z). nectar-core-v2.nectar.testnet holds 287 NYSE session windows and answers the session check (get_exit_capacity returns session_open and a reason). A closed-window refusal is tested in the sandbox only. The OutLayer calendar job has no recorded testnet execution. The earlier v1 contract still uses the owner flag market_session_open.
The rule
session_open = window_contains(now) ∧ (now − oracle_updated_ns ≤ 900 s) ∧ ¬forced_closed
When the session is closed:
D_c= 0 (DEX depth is ignored), andh_c=weekend_haircut_bpsinstead ofbase_haircut_bps.
Borrows still work, at the weekend parameters and under the normal oracle-freshness rule. See Cramér limits and Aggregate borrowing limit for where D_c and h_c enter.
It fails closed
Each condition can only close the session. The contract reports why with session_reason, checked in this order:
session_reason | Meaning |
|---|---|
no_calendar | the calendar is empty or was never set, so the session is closed |
outside_hours | now is not inside any NYSE window |
stale_price | the stored price is older than 900 s, or there is none |
forced_closed | the owner forced the asset closed |
open | none of the above |
force_session_closed(symbol, true) closes an asset (for halts or circuit breakers). force_session_closed(symbol, false) only clears the force. It cannot open a session outside a window or with a stale price. Nothing the owner sets can open a session.
Calendar windows
The contract does not compute time zones. It stores a sorted list of NYSE session windows [open_ns, close_ns) in UTC. Regular days and early closes are windows, and holidays are simply absent.
- Owner
set_session_calendar(windows)replaces the whole list. It panics unless the list is sorted and non-overlapping, hasopen < closein every window, and has at most 600 windows. View:get_session_calendar(from_index, limit). - The windows come from
packages/config/nyse-calendar.json, which holds the holidays and early closes read from nyse.com (F-342).scripts/22-session-calendar.mjsturns that file into windows, and it takes daylight saving time from the runtime's IANA data forAmerica/New_York. No window is typed by hand. - The calendar ends where F-342 ends (2027). After the last window, every session is closed until the owner loads a new list.
Why 900 s
The guard does not trust a price feed's own idea of "the market is open". Coinbase's tokenized-equity Chainlink feeds keep the last close through weekends and holidays and freeze during corporate actions (F-350). A held price looks fresh to a naive reader. Requiring a price published within 900 s while the window is open means a frozen feed closes the session.
Pending or not built (PRD §4.7)
- An attested OutLayer result that sets
forced_closed. The contract hook is built and tested in the sandbox:request_session_check(symbol)(anyone, 60 s cooldown) calls the OutLayer calendar job, andon_session_checkonly acceptsforce_closed: true; afalseanswer is ignored, so nothing outside the contract can open a session (tests_v7b::calendar_job_can_close_but_never_open, sandboxoutlayer_calendar_cannot_open_and_multiplier_bounded). The OutLayer job itself is built (quoter/src/calendar.rs, vectors in quoter/tests/jobs_vectors.json); no testnet execution of it is recorded in docs/EVIDENCE.md. - A weekend reference price. The v2 fields
weekend_ref_price_e6andweekend_ref_updated_nsexist and are unused. PROD would use a 24/7 DEX TWAP plus an implied-volatility spread.
Tests
tests_v7::empty_calendar_means_closedtests_v7::outside_hours_is_closedtests_v7::price_older_than_900s_flips_to_weekend_parameterstests_v7::forced_closed_and_false_cannot_opentests_v7::reason_precedence_outside_hours_before_stale_and_forcedtests_v7::calendar_set_and_paginate, plus the refusalscalendar_unsorted_rejected,calendar_overlap_rejected,calendar_empty_window_rejected,calendar_over_600_rejected,calendar_owner_onlymath::tests::limit_closed_session_ignores_depth_and_uses_weekend_haircut,math::tests::session_window_search- sandbox
weekend_guard_calendar_stale_and_forced(contracts/nectar-core/tests/sandbox_v7.rs)