# frozen_string_literal: true require "bucket" require "rate" # Generates realistic test data for ECB and BOC providers. # All dates are relative to today so tests never go stale. module Fixtures BASE_RATES = { "ECB" => { base: "EUR", quotes: { "USD" => 1.08, "GBP" => 0.86, "JPY" => 160.0, "CAD" => 1.47, "INR" => 90.0, "CHF" => 0.95, "SEK" => 11.2, "NOK" => 11.5, "PLN" => 4.3, "CZK" => 25.1, }, }, "BOC" => { base: "CAD", quotes: { "USD" => 0.74, "EUR" => 0.68, "GBP" => 0.58, "JPY" => 109.0 }, }, "BOJ" => { mixed: [ { base: "EUR", quote: "USD", rate: 1.08 }, { base: "USD", quote: "JPY", rate: 155.0 }, ], }, }.freeze # Number of business days to generate (~2 years covers downsampling and range tests) BUSINESS_DAYS = 520 class << self def seed! Rate.dataset.delete generate_rates.each_slice(1000) do |batch| Rate.dataset.multi_insert(batch) end rebuild_rollups! rebuild_currencies! end # The most recent business day in the fixture (useful for tests) def latest_date business_days.first end # A business day roughly N calendar days ago (guaranteed to be in the fixture) def business_day(days_ago) business_days.find { |d| d <= Date.today - days_ago } end # Find a recent Sunday for weekend snap tests def recent_sunday date = Date.today date -= 1 until date.sunday? date end # Find the preceding Friday for a given weekend date def preceding_friday(date) date -= 1 until date.friday? date end # A Monday in the fixture: the first publication day after a weekend gap # (mirrors the production "first publish after a holiday" scenario). def gap_boundary_monday(days_ago = 60) business_days.find { |d| d.monday? && d <= Date.today - days_ago } end private def rebuild_rollups! db = Sequel::Model.db db[:weekly_rates].delete week_bucket = Bucket.week db[:weekly_rates].insert( [:bucket_date, :provider, :base, :quote, :rate], db[:rates].select(week_bucket, :provider, :base, :quote, Sequel.function(:avg, :rate)) .group(:provider, :base, :quote, week_bucket), ) db[:monthly_rates].delete month_bucket = Bucket.month db[:monthly_rates].insert( [:bucket_date, :provider, :base, :quote, :rate], db[:rates].select(month_bucket, :provider, :base, :quote, Sequel.function(:avg, :rate)) .group(:provider, :base, :quote, month_bucket), ) end def rebuild_currencies! db = Sequel::Model.db db[:currencies].delete db.run(<<~SQL) INSERT INTO currencies (iso_code, start_date, end_date) SELECT iso_code, MIN(start_date), MAX(end_date) FROM ( SELECT quote AS iso_code, MIN(date) AS start_date, MAX(date) AS end_date FROM rates GROUP BY quote UNION ALL SELECT base AS iso_code, MIN(date) AS start_date, MAX(date) AS end_date FROM rates GROUP BY base ) GROUP BY iso_code ORDER BY iso_code SQL db[:currency_coverages].delete db.run(<<~SQL) INSERT INTO currency_coverages (provider_key, iso_code, start_date, end_date) SELECT provider, iso_code, MIN(date), MAX(date) FROM ( SELECT provider, quote AS iso_code, date FROM rates UNION ALL SELECT provider, base AS iso_code, date FROM rates ) GROUP BY provider, iso_code ORDER BY provider, iso_code SQL end def generate_rates days = business_days records = [] BASE_RATES.each do |provider, config| days.each do |date| jitter = 1.0 + (date.jd % 100 - 50) * 0.001 # deterministic jitter from date if config[:mixed] config[:mixed].each do |pair| records << { provider:, date:, base: pair[:base], quote: pair[:quote], rate: (pair[:rate] * jitter).round(4) } end else config[:quotes].each do |quote, rate| records << { provider:, date:, base: config[:base], quote:, rate: (rate * jitter).round(4) } end end end end records end def business_days @business_days ||= begin days = [] date = Date.today while days.size < BUSINESS_DAYS days << date unless date.saturday? || date.sunday? date -= 1 end days end end end end