Task: Holiday-aware date picker widget

Table of Contents

This page documents a task in the Calendar entity follow-ups: date picker, list-pagination fix, QuantLib materialization story. It captures the goal, current status, acceptance, and any notes or results.

Goal

A reusable Qt widget that lets a user pick a date while showing which dates are holidays, given one or more calendars as input (reusing calendar's holiday-adjustment data — Calendar entity follow-ups: date picker, list-pagination fix, QuantLib materialization). Holidays for the union of the input calendars are visually highlighted in the date grid (weekends included, following the same convention QuantLib's calendar classes use). When multiple calendars are given, it must be clear which calendar(s) a given highlighted date's holiday is coming from — e.g. a tooltip on hover naming the source calendar(s) — not just an undifferentiated highlight. This widget becomes the standard way to pick any date that is calendar-sensitive across the app (e.g. trade dates, fixing dates, spot dates) — not just within currency/currency-pair screens.

Depends on Model the calendar entity for codegen existing first, since the widget needs a real calendar → holiday-dates data source, not free text.

Also depends on Materialise QuantLib calendar holidays and support calendar adjustments: this widget reads from that task's materialised calendar_dates table (base QuantLib dates plus any calendar_adjustment overrides) rather than calling into QuantLib live from the UI.

Status

Field Value
State DONE
Parent story Calendar entity follow-ups: date picker, list-pagination fix, QuantLib materialization
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-07-30

Acceptance

  • [X] A reusable widget exists (HolidayAwareDatePicker, ores.qt.api) that accepts one or more calendar codes and highlights their non-business dates in the popup date grid.
  • [X] Weekends are highlighted using the same convention as holidays – calendar_date::is_business_day covers both, undistinguished by design (see its own doc comment), so the widget highlights every non-business date uniformly rather than re-deriving a separate weekend mask client-side.
  • [X] Hovering a highlighted date shows a tooltip naming which input calendar(s) it's non-business in — clear attribution when multiple calendars are given, not just an undifferentiated highlight.
  • [X] Widget is documented in the Qt UI patterns doc (new "Reusable input widgets" section), alongside its siblings OreDateEdit and CalendarAssignmentWidget, so other screens know to reuse it instead of a plain date picker for calendar-sensitive dates.
  • [X] At least one real usage wired up as a proof point: SwapInstrumentForm's fraFixingDateEdit (FRA Fixing Date).
  • [X] The date picker highlights materialised calendar_dates rows fetched from the server, not a live QuantLib computation — sourced from Materialise QuantLib calendar holidays and support calendar adjustments via fetch_calendar_holidays().

Plan

(Implementation strategy. Written when work starts; key decisions are distilled into the parent story's * Decisions at close, but the plan itself stays — it is the historical record of what we did.)

Data source: paged fetch of materialised calendar_dates, not a new bulk endpoint

The server already exposes refdata.v1.calendar_dates.list_by_calendar_code (offset/limit, ordered by date ascending – the same request the Browse Holidays list window uses). Rather than adding a new bulk "give me all holidays" endpoint, fetch_calendar_holidays() (ores.qt.api) pages through the existing request per calendar code, stopping as soon as a page's dates run past the requested window's end – cheap in the common case (a few hundred rows at most for a practical window) and reuses proven server-side plumbing rather than adding a parallel data path.

Window sizing: lazy, bounded, not the full materialisation horizon

calendar_dates is materialised out to a multi-decade horizon (default 2050) per the parent materialisation task – eagerly fetching that whole range per calendar for every date picker would be wasted work for the overwhelming common case (a date a few months to a couple of years out). The widget instead loads a practical window (1 year back, 5 years forward) around whichever month its popup happens to be showing, expanding on demand as the user navigates further via QCalendarWidget::currentPageChanged. A curve reaching decades out (e.g. a 30Y swap) would need to navigate the popup that far to see highlighting that far – a deliberate, documented trade-off of eager-fetch cost against picker responsiveness, not an oversight.

Highlight mechanism: setDateTextFormat, not a custom paintCell override

QCalendarWidget::setDateTextFormat() (a supported public API for per-date styling) does the highlighting – no need to subclass QCalendarWidget and override its paint logic. Tooltips, which QCalendarWidget has no built-in per-cell API for, use an event filter on its internal QAbstractItemView (QEvent::ToolTip, reconstructing the hovered cell's date from the view's model index, the shown month/year, and firstDayOfWeek() – the model has no header rows/columns of its own, headers are drawn by the view separately).

Real usage: SwapInstrumentForm's FRA Fixing Date, with a static TARGET default

fraFixingDateEdit (SwapInstrumentForm.ui) swapped from OreDateEdit to HolidayAwareDatePicker as the proof point. Its calendar is hardcoded to TARGET rather than derived from the form's own currency selection: no client-facing currency-to-calendar lookup exists today – currency_calendar is a junction with no NATS-exposed service/protocol layer of its own (the same precedent CalendarAssignmentWidget's own doc comment already documents), so deriving it dynamically would be new, unrelated scope. Documented as a known simplification in the wiring code itself, not silently passed off as the finished article.

Notes

Requested again during manual QA of the calendar Qt screens task (2026-07-19): tester asked for "some kind of hand crafted notebook page to show the holidays for the calendar" when reviewing a CalendarDetailDialog row. This task's scope (holiday-aware picker, reusable everywhere a calendar-sensitive date is entered) covers that need; no longer blocked, since Model the calendar entity for codegen has landed.

Manual verification: partial

Launched the Qt client (compass client start) against the running dev environment – the app started cleanly, loaded every plugin (including ores.qt.trading, which now links the widget), and reached the login screen with no crash, confirming the new code loads and runs, not just compiles. Did not click through to SwapInstrumentForm and visually confirm the popup calendar's highlighting/tooltip: this environment has no display/screenshot tool available to this session to drive or observe the GUI interactively. Flagging honestly rather than claiming a full manual click-through that didn't happen – a human with display access (or a future screenshot-capable tool) should do that pass before/soon after merge.

PRs

PR Title
#1772 [agile] Fix stale sprint.org row for calendar-followups story
#1769 [qt] Add HolidayAwareDatePicker reusable calendar-sensitive date widget

Review

# Comment summary File Decision Notes
1 (PR #1769, x4 reviewers) Stale highlights + stale in-flight fetch survive a setCalendarCodes() re-call with different codes HolidayAwareDatePicker.cpp/.hpp Fixed Reset previously-highlighted dates' QTextCharFormat in setCalendarCodes(); added a generation counter, bumped per setCalendarCodes() call and checked in loadWindow()'s finished handler, so a stale in-flight fetch's result is discarded rather than merged in.
2 Window expansion re-fetches the whole merged range from scratch each time (protocol has no date-range filter, only offset/limit) HolidayAwareDatePicker.cpp, CalendarHolidayFetcher.cpp Declined Not a correctness bug – flagged as a protocol-level limitation, not fixable client-side without a new server-side date filter, which is separate scope from this widget.
3 errorMessage signal emitted but unwired by SwapInstrumentForm SwapInstrumentForm.cpp Declined Intentional fail-open UX for the proof-point wiring; a real consumer would connect it, same as CalendarAssignmentWidget's own errorMessage/statusMessage signals.
4 cellDateAt() relies on undocumented QCalendarWidget internal view/model layout HolidayAwareDatePicker.cpp Declined Correct as flagged – no public Qt API exists for "date at view index"; already commented in the code as a deliberate reliance on current Qt behaviour.

Result

All six acceptance criteria met. Shipped HolidayAwareDatePicker (ores.qt.api), a drop-in OreDateEdit replacement that highlights non-business dates in its popup calendar for one or more calendars, sourced from the server's materialised calendar_dates table via a new fetch_calendar_holidays() helper (paged, windowed, never a live QuantLib call). Tooltips on hover name which calendar(s) a highlighted date applies to. Documented in the Qt UI patterns doc alongside its siblings; wired into SwapInstrumentForm's FRA Fixing Date field as the proof point.

Two deliberate, documented scope limits (see Plan):

  • The widget loads a practical window (1y back / 5y forward) around the popup's shown month, not the full multi-decade materialisation horizon, expanding lazily as the user navigates further.
  • The proof-point wiring uses a static TARGET calendar default rather than deriving it from the form's selected currency – no client-facing currency-to-calendar lookup exists yet (currency_calendar has no NATS service/protocol layer, matching CalendarAssignmentWidget's own precedent); wiring that up is new, separate scope.

Verified: full local build clean (linux-clang-debug-make, including ores.qt.exe linking); ctest 72/74 passed, excluding the two pre-existing, already-captured ores.iam failures unrelated to this change. Manual verification was partial: the Qt client launches cleanly with the new widget's code loaded (confirmed via compass client start), but this session had no display/screenshot tool to click through and visually confirm the popup's highlighting/ tooltip – flagged honestly rather than claimed. A human (or a future screenshot-capable session) should do that pass.

Emacs 29.3 (Org mode 9.6.15)