Skip to content

Instantly share code, notes, and snippets.

@mkrcah
Created November 18, 2023 21:16
Show Gist options
  • Save mkrcah/32bbd41b3da95853474d92d8649b36bc to your computer and use it in GitHub Desktop.
Save mkrcah/32bbd41b3da95853474d92d8649b36bc to your computer and use it in GitHub Desktop.
find-exchange-rates (v2)
(ns exchange-rates.find-exchange-rate
(:require [clj-http.client :as client]
[tick.core :as t]
[clojure.tools.logging :as log]
[clojure.spec.alpha :as s]
[exchange-rates.rates-repo :as repo]
[exchange-rates.value-objects :as vo])
(:use [clojure.tools.trace]))
; alternatively use http://fx.modfin.se/
(defn- fetch-from-public-api
[pair period]
{:pre [(s/valid? ::vo/pair pair)
(<= (t/between (:start period) (:end period) :days) 90)]}
(let [url (str "http://api.frankfurter.app/" (:start period) ".." (:end period))]
(log/info "Fetching exchange rates from Frankfurter" period)
(client/get url
{:accept :json
:as :json
:query-params {:to (:quote pair)
:from (:base pair)}
:throw-exceptions true}))) ; FIXME - handle path
(defn- fetching-period [date]
"Prepare start and end date for fetching to save API requests"
{:start (t/<< date (t/of-days 3))
:end (t/>> date (t/of-days 70))})
(defn- latest-day-with-open-exchange
"Find the latest day before a given day when the exchange was opened"
[date]
(condp = (t/day-of-week date)
t/SUNDAY (t/<< date (t/of-days 2))
t/SATURDAY (t/<< date (t/of-days 1))
date))
(defn- in-past? [date]
(t/<= (t/date date) (t/today)))
(defn- response->rates
"Extract rates from the API response and convert
to the model expected by the repository"
[pair api-response]
(let [rates (-> api-response
:body
:rates)]
(into {} (map (fn [[k v]] [(name k) (get v (keyword (:quote pair)))]) rates))))
(defn by-date
"For a given date and currency pair, find European Central Bank exchange rate"
[pair requested-date-str]
{:pre [(in-past? (t/date requested-date-str))
(s/valid? ::vo/pair pair)]
:post [(or (nil? %) (number? %))]}
(let [actual-date (latest-day-with-open-exchange (t/date requested-date-str))
actual-date-str (str actual-date)]
(or (repo/get-by-date pair actual-date-str)
(do
(log/debug "Exchange rate not found in storage, fetching")
(-> actual-date
fetching-period
((partial fetch-from-public-api pair))
((partial response->rates pair))
((partial repo/store pair)))
(repo/get-by-date pair actual-date-str)))))
(comment
(def EUR-CZK {:base "EUR" :quote "CZK"})
(do
(def api-response
{:body {:rates {:2023-01-24 {:CZK 23.874},
:2023-01-25 {:CZK 23.809}}}})
(response->rates EUR-CZK api-response)
; fake fetch
(defn fetch-from-public-api [pair period]
api-response)
(by-date EUR-CZK "2023-01-24"))
(by-date EUR-CZK "2023-11-11")
(latest-day-with-open-exchange (t/date "2023-01-15")))
(ns exchange-rates.rates-repo
(:require [clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[exchange-rates.value-objects :as vo]
[tick.core :as t]
[clojure.test :refer [is]])
(:use [clojure.tools.trace]))
(def ^:private stored-rates (atom {}))
(defn store [pair rates]
{:pre [(is (s/valid? ::vo/pair pair))
(is (s/valid? (s/map-of string? double?) rates))]}
(log/debug "Storing" (count rates) (vo/pair->str pair) "rate(s)")
(swap! stored-rates into {pair rates}))
(defn get-by-date [pair date-str]
{:pre [(is (s/valid? ::vo/pair pair))
(is (string? date-str))]}
(log/debug "Getting" (vo/pair->str pair) "rate for" (str date-str))
(get (get @stored-rates pair) date-str))
(comment
(def CZK-EUR {:base "CZK" :quote "EUR"})
(s/explain ::vo/pair CZK-EUR)
(def today (str (t/date)))
(get-by-date CZK-EUR today)
(store CZK-EUR {today 13.4})
(get-by-date CZK-EUR today))
(ns exchange-rates.value-objects
(:require [clojure.spec.alpha :as s]))
(s/def ::currency-code
(s/and string? #(re-matches #"^[A-Z]{3}$" %)))
(s/def ::base ::currency-code)
(s/def ::quote ::currency-code)
(s/def ::pair
(s/keys :req-un [::base ::quote]))
(defn pair->str [pair]
{:pre [(s/valid? ::pair pair)]}
(str (:base pair) "-" (:quote pair)))
(comment
(s/valid? ::pair
{:base "EUR"
:quote "CZK"})
(s/valid? ::pair
{:base :EUR
:quote :CZK})
(def CZK-EUR {:base "CZK" :quote "EUR"})
(pair->str CZK-EUR))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment