BMD Pegged, CAD Pinned: How InsightUW Quotes in Three Currencies Without Forking the Rating Engine
From "we quoted CAD 1,200,000, the rate moved 80 bps overnight, the PDF the broker has shows USD 880,000 but the bound policy is at USD 876,000 — which one's right?" to "the rate is pinned at finalize, the PDF reads CAD 1,200,000 (display USD 876,000 at pinned 0.730), and the PAS push uses the same pinned rate days later" — through one column on the quote, a time-series rate table separate from the existing snapshot config, and the deliberate decision to display in native first.
The Problem
A Bermuda-admitted carrier writes an Acme Holdings D&O quote in CAD 1,200,000 on Tuesday. The CAD↔USD rate that morning was 0.7340. The broker reads the quote letter, shows the CFO, asks for changes. Two days later the broker accepts. The carrier's UW finalizes the manuscript wording on Thursday. Thursday's rate is 0.7295. The PDF the broker has on file displays USD 880,800; the carrier's PAS push lands at USD 875,400 (Thursday's rate × CAD 1,200,000). Six dollars per dollar of premium of mismatch.
Reconciliation flags the difference next quarter. Finance can't tell which number is "right." The audit story doesn't say. The broker emails: "Why does my copy say something different from the binder?"
The usual fixes don't fix:
- Always display in USD. Erases the broker's quote-letter currency. Bermuda brokers expect USD; Canadian brokers expect CAD; brokers shouldn't have to know what InsightUW thinks the conversion is on a given Tuesday.
- Always quote in USD. Doesn't match how Bermuda + Canadian markets actually trade. Loses business.
- Recompute USD on every page load. Rate flicker. The broker sees one number Tuesday, a different one Thursday — same quote, different display.
- Per-product rate tables. Hand-curate seventy of these and watch them drift.
The root cause: there's no single moment where the system says "here's the rate for this quote, this version, frozen." Without that pin, every consumer (display, PDF render, PAS push, comparison sheet) makes its own decision about today's rate, and the decisions diverge.
The InsightUW Approach
Pin the rate at finalize. Store native premium. Display in native first. Convert on demand using the pinned rate. The rate is a property of the quote, not the display.
Fx Rate is a time series, not a snapshot
The codebase already had Currency Config — a single-row-per-currency snapshot config table ("we trade in CAD, here's the rate"). That's wrong for audit. Fx Rate adds a real time series:
One row per (currency_pair, rate_date, source). Lookup pattern: "rate for CAD→USD on or before 2026-05-12, prefer source wm reuters over manual." The fallback chain reads:
- Direct — Fx Rate row matching (from, to, ≤ as_of).
- USD pivot — for non-USD pairs without a direct row, route via USD: from→USD ÷ to→USD.
- Legacy —
Currency Config.exchange_rate_to_usdsnapshot. Environments without a feed yet still work.
The fallback ordering matters: an installed customer can keep using the old snapshot config while gradually onboarding Fx Rate daily entries. No migration moment.
BMD↔USD is permanently 1.0
Bermuda Dollar is pegged to USD. We seed Fx Rate(BMD, USD, 1.0, source='peg') and Fx Rate(USD, BMD, 1.0, source='peg') once. The peg source is a hint: don't accumulate daily-feed entries for this pair; the rate isn't moving. The seed notes field carries the "Bermuda Dollar pegged 1:1 to USD" comment so a future maintainer doesn't think it's a bug.
Pin-on-finalize, not pin-on-create
A draft quote doesn't have a pinned rate. The UW edits, revises, plays with options. The rate would mean nothing — there's nothing committed yet. The pin happens when the manuscript form finalizes (capability #3) — the moment a broker-facing artefact is locked. Pinning earlier would be premature; pinning later would mean the PDF the broker has differs from the PAS push days later.
Quote Form Service.finalize calls pin rate for quote:
- USD quotes: no-op. Rate is 1.0; no need to write it down.
- CAD/EUR/etc.: idempotent. If fx rate to usd at issue is already populated, skip. First finalize wins.
- No rate available for the pair on/before today: 409 with a hint. The form is already locked and rendered — the audit trail records the pin attempt failed, the manager retries via
/api/uw_fx/quotes/{guid}/pinafter the admin loads the day's rate.
Three columns on Quote:
Display: native first, USD on toggle
The quote-manager detail view defaults to native. CA$1.2M reads as CA$1.2M. A toggle pill — "Native | USD" — flips the display to USD-converted, using the pinned rate. The toggle hides on USD quotes (no-op) and disables when a rate isn't pinned (draft state).
The badge under the toggle reads "pinned rate 0.7340 CAD→USD at Tue 11:42 UTC." The auditor sees the math.
Manuscript render: currency-aware Jinja filter
The existing currency Jinja filter took no arguments — {{ premium | currency }} always rendered $. Now:
A symbol map handles the common cases (USD/BMD=$, CAD=CA$, GBP=£, EUR=€); unknown ISO codes fall through as "<CODE> " prefix (e.g., SGD 1,200,000). Manuscripts authored before #13 still work — no argument means "USD" which means "$" — the legacy default is exactly the legacy behaviour.
build quote context surfaces quote.currency
The merge-token catalogue (capability #3's sidebar) gains a quote.currency token. The build quote context helper exposes both quote.currency and quote.fx_rate_to_usd_at_issue. The same context manuscripts merge against; the same context the email composer's preview pane reads; one source of currency truth.
Phase 1 — and what's deferred
We deliberately scoped #13 to Phase 1: Quote-only. The other 50-something money-typed columns (Policy, Claim, Insured.total_premium, Broker.book_premium, every aggregator) stay USD-implicit for now.
Why phase it? Multi-currency is a horizontal touch — every aggregation rule, every authority gate, every report would need to learn currency. Doing it all at once would take a quarter. Doing the quote part — the part the customer asked for — takes ~1100 LOC and ships independently.
Phase 2 (deferred): Currency on Policy (cascades from quote on bind), Claim (inherits from policy), aggregations (Broker.book_premium across mixed-currency policies), authority matrix currency scoping (so "$5M premium authority in USD" doesn't clear "$5M premium quote in CAD").
Phase 3 (deferred): Rating engine multi-currency. Rating produces a number; the quote captures the currency. The conversion belongs in display + push, not rating.
Worked Example: Acme Holdings D&O, CAD
Sarah is renewing Acme's D&O on the Bermuda branch. The carrier writes Bermuda risks in CAD for Canadian-domiciled accounts; Acme is one.
Step 1 — Currency on quote create
She opens New quote. The form has a Currency dropdown (USD / CAD / BMD / GBP / EUR). She picks CAD. The form labels update: "Premium (CAD)", "Total Limit (CAD)", etc. She enters:
She saves. The Quote row stores currency_code='CAD', premium=1_200_000, fx_rate_to_usd_at_issue=NULL (draft).
Step 2 — Compose the manuscript
She opens Add form → From library → "Bermuda Public D&O — Manuscript v3.2". The editor opens; the merge-field sidebar offers {{ quote.premium | currency(quote.currency) }}. She writes the wording, finalizes:
Step 3 — Email the broker
She opens the broker email composer (capability #4). The seeded quote email template renders:
The merge-fields render in CAD because build quote context exposes quote.currency and the email template uses {{ quote.premium | currency(quote.currency) }}. The PDF attached to the email reads CA$ throughout. The broker sees CAD; that's the right answer for their workflow.
Step 4 — Quote-manager detail view
Sarah opens the quote detail tomorrow. The Premium tile shows CA$1.2M. Right of the tile: a small chip — CAD (the active toggle). Below: the toggle pill — CAD (native) | USD (converted). She clicks USD. The display flips to $876k (1.2M × 0.7340). Subtitle: "pinned rate 0.7340 CAD→USD at 2026-05-12 11:42 UTC."
Same row, two views. Storage stays in CAD; display converts.
Step 5 — Rate moves; quote unchanged
Three days later the CAD↔USD rate is 0.7295. Sarah reopens the quote. The toggle still reads "pinned rate 0.7340" — the system uses the pinned rate, not today's. The PDF in the broker's inbox still reads CA$1,200,000 / USD $876,000. Audit story intact.
The rate-admin admin runs /api/uw_fx/rates POST with the day's WM/Reuters fix. New Fx Rate row written. Future quotes pinning today get 0.7295. Acme's quote still pins to 0.7340.
Step 6 — Bind; PAS push uses the pinned rate
The broker accepts. Bind triggers queue policy create (capability #8). The PAS dispatch context reads:
The connector's field mapping:
Post-mapping payload to policy admin system:
policy admin system receives the same numbers the broker has on the quote letter. Reconciliation has nothing to flag.
Step 7 — A USD policy via BMD branch
The same UW writes another account a week later — Bermuda-domiciled, USD-quoted. She picks BMD on the currency dropdown. The display chip reads BMD; the symbol is $ (BMD glyph matches USD). On finalize:
The toggle on the detail view? Disabled. The display already reads $1.2M; converting it to USD also reads $1.2M. The system surfaces "BMD pegged 1:1 to USD" as a tooltip on the chip so the auditor knows the rate isn't a coincidence.
What This Means for Underwriters
- Storage stays native. A CAD quote stores premium in CAD. No dual-storage, no recomputation. The system trusts what was entered.
- The pin is the truth. Once finalized, the rate is locked. Daily FX moves don't retroactively change quote economics. The broker's PDF and the PAS record agree, days or months later.
- Display is a presentation layer. Toggle to USD when convenient; the underlying value is unchanged.
- BMD is permanent 1.0. Bermuda Dollar peg modeled as
source='peg'; the toggle hides because there's nothing to convert. - Merge fields render the right symbol.
{{ quote.premium | currency(quote.currency) }}does the right thing for USD ($), BMD ($), CAD (CA$), GBP (£), EUR (€), or any ISO code that falls through to the prefix path (SGD 1,200,000). - Phase 1 is shippable on its own. Quote-only. Policy / Claim / aggregations / authority gates / rating-engine multi-currency are deferred. Each is a separate scoping conversation.
- The rate-admin is a settings page, not a daemon. Daily snapshots loaded by an admin or a tiny cron POSTing the day's fix. Live feeds are a Phase 3 concern.
- Legacy callers keep working. The Jinja
currencyfilter defaults to USD if no argument is passed. The AngularformatCurrencydefaults to USD. Old code on USD-only quotes is unaffected; new code threads the currency.
What's Next
Want to see how a single column on the quote ends the "which rate is the right one?" reconciliation problem? Request a demo.