tests/testthat/test-boe_curve.R

# Tests for boe_curve() and boe_curve_panel().
# Network-dependent paths are skipped on CRAN and offline.

# ---- argument validation (offline, no network) ------------------------------

test_that("boe_curve rejects invalid curve / measure / frequency", {
  expect_error(boe_curve(curve = "foo"))
  expect_error(boe_curve(measure = "par"))
  expect_error(boe_curve(frequency = "weekly"))
})

test_that("boe_curve rejects from > to", {
  expect_error(
    boe_curve(curve = "nominal", from = "2020-01-01", to = "2019-01-01"),
    regexp = "must be before"
  )
})

test_that("boe_curve_panel rejects non-positive maturities", {
  expect_error(boe_curve_panel(maturities = c(-1, 5)))
  expect_error(boe_curve_panel(maturities = c(0, 5)))
})

test_that("boe_curve and boe_curve_panel reject invalid segment", {
  expect_error(boe_curve(segment = "frontend"))
  expect_error(boe_curve_panel(segment = "frontend"))
})

# ---- short-end sheet selection (offline, pure function) ----------------------

test_that("yield_sheet_pattern selects sheets by measure and segment", {
  # Standard sheets (modern + curve-name-infixed older layout).
  expect_match("4. spot curve",         yield_sheet_pattern("spot", "standard"))
  expect_match("4. nominal spot curve", yield_sheet_pattern("spot", "standard"))
  expect_match("2. fwd curve",          yield_sheet_pattern("forward", "standard"))

  # Short-end sheets: spot uses "spot, short end"; forward is plural "fwds".
  expect_match("3. spot, short end",         yield_sheet_pattern("spot", "short"))
  expect_match("3. nominal spot, short end", yield_sheet_pattern("spot", "short"))
  expect_match("1. fwds, short end",         yield_sheet_pattern("forward", "short"))
  expect_match("1. nominal fwds, short end", yield_sheet_pattern("forward", "short"))
})

test_that("yield_sheet_pattern does not cross-match segments", {
  expect_false(grepl(yield_sheet_pattern("spot", "standard"),
                     "3. spot, short end", ignore.case = TRUE))
  expect_false(grepl(yield_sheet_pattern("spot", "short"),
                     "4. spot curve", ignore.case = TRUE))
  expect_false(grepl(yield_sheet_pattern("forward", "standard"),
                     "1. fwds, short end", ignore.case = TRUE))
})

# ---- panel pillar labelling (offline, mocked boe_curve) ---------------------

test_that("boe_curve_panel keeps labels aligned when a pillar is dropped", {
  fake_long <- new_boe_tbl(
    data.frame(
      date           = rep(as.Date(c("2020-01-01", "2020-01-02")), each = 3),
      maturity_years = rep(c(1, 2, 5), times = 2),
      rate_pct       = c(1.1, 2.2, 5.5, 1.2, 2.3, 5.6)
    ),
    query = list(function_name = "boe_curve", source = "latest")
  )
  testthat::local_mocked_bindings(boe_curve = function(...) fake_long)

  # Pillar 10 is absent from the (1, 2, 5) grid, so it must drop without
  # shifting the labels of the surviving columns.
  expect_warning(
    panel <- boe_curve_panel(curve = "nominal", maturities = c(1, 2, 5, 10)),
    regexp = "not on the"
  )
  expect_named(panel, c("date", "m1", "m2", "m5"))
  expect_equal(panel$m1[panel$date == as.Date("2020-01-01")], 1.1)
  expect_equal(panel$m5[panel$date == as.Date("2020-01-01")], 5.5)
})

# ---- network-dependent: short end -------------------------------------------

test_that("boe_curve fetches the short end with monthly maturities", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_short_"))
  on.exit(options(op), add = TRUE)

  out <- boe_curve(curve = "nominal", measure = "spot", segment = "short")

  expect_s3_class(out, "boe_tbl")
  expect_gt(nrow(out), 50L)
  expect_lt(min(out$maturity_years), 0.5)    # sub-6-month points exist
  expect_lte(max(out$maturity_years), 5.01)  # short end caps near 5 years

  q <- attr(out, "boe_query")
  expect_equal(q$segment, "short")
  expect_match(q$series_codes, "AS_NOMINAL_SPOT_SHORT")
})

test_that("short end caps lower than standard; forward short end parses", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_short2_"))
  on.exit(options(op), add = TRUE)

  std   <- boe_curve(curve = "nominal", measure = "spot", segment = "standard")
  short <- boe_curve(curve = "nominal", measure = "spot", segment = "short")
  expect_lt(max(short$maturity_years), max(std$maturity_years))

  fwd <- boe_curve(curve = "nominal", measure = "forward", segment = "short")
  expect_gt(nrow(fwd), 50L)
})

test_that("OIS short end skips periods without a short-end sheet", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_ois_short_"))
  on.exit(options(op), add = TRUE)

  # OIS publishes no short end for 2009-2015; requesting from 2009 must skip
  # those workbooks cleanly rather than error, and still return later data.
  out <- boe_curve(curve = "ois", measure = "spot", segment = "short",
                   from = "2009-01-01")
  expect_s3_class(out, "boe_tbl")
  expect_gt(nrow(out), 0L)
  expect_gt(min(out$date), as.Date("2010-01-01"))
})

test_that("boe_curve_panel short segment returns short-end pillars", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_panel_short_"))
  on.exit(options(op), add = TRUE)

  panel <- boe_curve_panel(curve = "nominal", measure = "spot",
                           segment = "short", maturities = c(1, 2, 5))
  expect_s3_class(panel, "boe_tbl")
  expect_named(panel, c("date", "m1", "m2", "m5"))
  expect_gt(nrow(panel), 10L)
})

# ---- archive URL registry (offline) -----------------------------------------

test_that("yield_archive_registry has 5 curves x 2 frequencies", {
  reg <- yield_archive_registry()
  expect_named(reg, c("daily", "monthly"))
  expect_named(reg$daily,
               c("nominal", "real", "inflation", "ois", "blc"),
               ignore.order = TRUE)
  expect_named(reg$monthly,
               c("nominal", "real", "inflation", "ois", "blc"),
               ignore.order = TRUE)
})

test_that("yield_archive_url constructs valid URLs", {
  u <- yield_archive_url("nominal", "daily")
  expect_match(u, "^https://www.bankofengland.co.uk/.*\\.zip$")
  expect_match(u, "glcnominalddata.zip$")

  u2 <- yield_archive_url("ois", "monthly")
  expect_match(u2, "oismonthedata.zip$")

  u3 <- yield_archive_url("blc", "daily")
  expect_match(u3, "blcnomddata.zip$")
})

test_that("yield_archive_url errors on unknown curve", {
  expect_error(yield_archive_url("treasury", "daily"))
})

# ---- maturity-row detection (offline, synthetic) ----------------------------

test_that("detect_maturity_row finds row 4 in modern layout", {
  raw <- data.frame(
    matrix(c(
      "title", NA, NA, NA, NA, NA,
      "Maturity ", NA, NA, NA, NA, NA,
      "months:", "6", "12", "24", "36", "60",
      "years:", 0.5, 1, 2, 3, 5,
      NA, NA, NA, NA, NA, NA,
      "1970-01-31", 1.0, 1.1, 1.2, 1.3, 1.4
    ), nrow = 6, byrow = TRUE),
    stringsAsFactors = FALSE
  )
  expect_equal(detect_maturity_row(raw), 4L)
})

test_that("detect_maturity_row falls back when row 4 is empty", {
  raw <- data.frame(
    matrix(c(
      "title", NA, NA, NA, NA, NA,
      "years:", 0.5, 1, 2, 3, 5,           # row 2 holds maturities
      NA, NA, NA, NA, NA, NA,
      NA, NA, NA, NA, NA, NA,              # row 4 is empty
      NA, NA, NA, NA, NA, NA,
      "1970-01-31", 1.0, 1.1, 1.2, 1.3, 1.4
    ), nrow = 6, byrow = TRUE),
    stringsAsFactors = FALSE
  )
  out <- detect_maturity_row(raw)
  expect_true(is.na(out) || out %in% c(3L, 5L, 6L))
})

# ---- network-dependent: latest month (default behaviour) --------------------

test_that("boe_curve fetches and parses the nominal spot curve", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_curve_"))
  on.exit(options(op), add = TRUE)

  out <- boe_curve(curve = "nominal", measure = "spot")

  expect_s3_class(out, "boe_tbl")
  expect_true(all(c("date", "maturity_years", "rate_pct") %in% names(out)))
  expect_gt(nrow(out), 50L)
  expect_true(all(out$maturity_years >= 0.5))
  expect_true(all(is.finite(out$rate_pct)))

  q <- attr(out, "boe_query")
  expect_equal(q$function_name, "boe_curve")
  expect_equal(q$source, "latest")
  expect_match(q$series_codes, "AS_NOMINAL_SPOT")
})

test_that("boe_curve forward measure works for nominal", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_curve_fwd_"))
  on.exit(options(op), add = TRUE)

  out <- boe_curve(curve = "nominal", measure = "forward")
  expect_s3_class(out, "boe_tbl")
  expect_gt(nrow(out), 50L)
})

test_that("boe_curve fetches inflation and OIS curves", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_curve_misc_"))
  on.exit(options(op), add = TRUE)

  inflation <- boe_curve(curve = "inflation", measure = "spot")
  expect_s3_class(inflation, "boe_tbl")
  expect_gt(nrow(inflation), 0L)

  ois <- boe_curve(curve = "ois", measure = "spot")
  expect_s3_class(ois, "boe_tbl")
  expect_gt(nrow(ois), 0L)
})

# ---- network-dependent: archive paths ---------------------------------------

test_that("boe_curve fetches monthly archive for OIS (smallest zip)", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_curve_arc_"))
  on.exit(options(op), add = TRUE)

  out <- boe_curve(curve = "ois", measure = "spot",
                   frequency = "monthly", from = "2015-01-01")

  expect_s3_class(out, "boe_tbl")
  expect_gt(nrow(out), 100L)
  expect_true(min(out$date) >= as.Date("2015-01-01"))

  q <- attr(out, "boe_query")
  expect_equal(q$source, "archive")
  expect_equal(q$frequency, "monthly")
})

test_that("boe_curve_panel returns wide format with requested pillars", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_panel_"))
  on.exit(options(op), add = TRUE)

  # OIS only extends to ~5y so test pillars stay within that range
  panel <- boe_curve_panel(
    curve     = "ois",
    measure   = "spot",
    frequency = "monthly",
    from      = "2015-01-01",
    maturities = c(1, 3, 5)
  )

  expect_s3_class(panel, "boe_tbl")
  expect_named(panel, c("date", "m1", "m3", "m5"))
  expect_gt(nrow(panel), 100L)
  expect_true(all(is.finite(panel$m5) | is.na(panel$m5)))
})

test_that("boe_curve fetches BLC daily latest", {
  testthat::skip_on_cran()
  testthat::skip_if_offline()
  testthat::skip_if_not_installed("readxl")

  op <- options(boe.cache_dir = tempfile("boe_blc_"))
  on.exit(options(op), add = TRUE)

  out <- boe_curve(curve = "blc", measure = "spot")
  expect_s3_class(out, "boe_tbl")
  expect_gt(nrow(out), 50L)
})

Try the boe package in your browser

Any scripts or data that you put into this service are public.

boe documentation built on May 30, 2026, 5:06 p.m.