Support Bank of Canada daily exchange rates.
This commit is contained in:
parent
039d7fb809
commit
947eaacd29
10 changed files with 818 additions and 32 deletions
|
@ -22,6 +22,7 @@ pipx install pricehist
|
|||
## Sources
|
||||
|
||||
- **`alphavantage`**: [Alpha Vantage](https://www.alphavantage.co/)
|
||||
- **`bankofcanada`**: [Bank of Canada daily exchange rates](https://www.bankofcanada.ca/valet/docs)
|
||||
- **`coinbasepro`**: [Coinbase Pro](https://pro.coinbase.com/)
|
||||
- **`coindesk`**: [CoinDesk Bitcoin Price Index](https://www.coindesk.com/coindesk-api)
|
||||
- **`coinmarketcap`**: [CoinMarketCap](https://coinmarketcap.com/)
|
||||
|
|
53
poetry.lock
generated
53
poetry.lock
generated
|
@ -415,11 +415,11 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytes
|
|||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.3"
|
||||
version = "1.5.1"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
|
@ -749,36 +749,25 @@ tox = [
|
|||
{file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
|
||||
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
|
||||
{file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"},
|
||||
{file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"},
|
||||
{file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"},
|
||||
{file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"},
|
||||
{file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"},
|
||||
{file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"},
|
||||
{file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"},
|
||||
{file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"},
|
||||
{file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"},
|
||||
{file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"},
|
||||
{file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"},
|
||||
{file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"},
|
||||
{file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"},
|
||||
{file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"},
|
||||
{file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"},
|
||||
{file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"},
|
||||
{file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"},
|
||||
{file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"},
|
||||
{file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||
|
|
4
src/pricehist/beanprice/bankofcanada.py
Normal file
4
src/pricehist/beanprice/bankofcanada.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from pricehist import beanprice
|
||||
from pricehist.sources.bankofcanada import BankOfCanada
|
||||
|
||||
Source = beanprice.source(BankOfCanada())
|
|
@ -1,4 +1,5 @@
|
|||
from .alphavantage import AlphaVantage
|
||||
from .bankofcanada import BankOfCanada
|
||||
from .coinbasepro import CoinbasePro
|
||||
from .coindesk import CoinDesk
|
||||
from .coinmarketcap import CoinMarketCap
|
||||
|
@ -9,6 +10,7 @@ by_id = {
|
|||
source.id(): source
|
||||
for source in [
|
||||
AlphaVantage(),
|
||||
BankOfCanada(),
|
||||
CoinbasePro(),
|
||||
CoinDesk(),
|
||||
CoinMarketCap(),
|
||||
|
|
118
src/pricehist/sources/bankofcanada.py
Normal file
118
src/pricehist/sources/bankofcanada.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
import dataclasses
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import requests
|
||||
|
||||
from pricehist import exceptions
|
||||
from pricehist.price import Price
|
||||
|
||||
from .basesource import BaseSource
|
||||
|
||||
|
||||
class BankOfCanada(BaseSource):
|
||||
def id(self):
|
||||
return "bankofcanada"
|
||||
|
||||
def name(self):
|
||||
return "Bank of Canada"
|
||||
|
||||
def description(self):
|
||||
return "Daily exchange rates of the Canadian dollar from the Bank of Canada"
|
||||
|
||||
def source_url(self):
|
||||
return "https://www.bankofcanada.ca/valet/docs"
|
||||
|
||||
def start(self):
|
||||
return "2017-01-03"
|
||||
|
||||
def types(self):
|
||||
return ["default"]
|
||||
|
||||
def notes(self):
|
||||
return (
|
||||
"Currently, only daily exchange rates are supported. They are "
|
||||
"published once each business day by 16:30 ET. "
|
||||
"All Bank of Canada exchange rates are indicative rates only.\n"
|
||||
"To request support for other data provided by the "
|
||||
"Bank of Canada Valet Web Services, please open an "
|
||||
"issue in pricehist's Gitlab project. "
|
||||
)
|
||||
|
||||
def symbols(self):
|
||||
url = "https://www.bankofcanada.ca/valet/lists/series/json"
|
||||
|
||||
try:
|
||||
response = self.log_curl(requests.get(url))
|
||||
except Exception as e:
|
||||
raise exceptions.RequestError(str(e)) from e
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
raise exceptions.BadResponse(str(e)) from e
|
||||
|
||||
try:
|
||||
data = json.loads(response.content)
|
||||
series_names = data["series"].keys()
|
||||
fx_series_names = [
|
||||
n for n in series_names if len(n) == 8 and n[0:2] == "FX"
|
||||
]
|
||||
results = [
|
||||
(f"{n[2:5]}/{n[5:9]}", data["series"][n]["description"])
|
||||
for n in sorted(fx_series_names)
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
raise exceptions.ResponseParsingError(str(e)) from e
|
||||
|
||||
if not results:
|
||||
raise exceptions.ResponseParsingError("Expected data not found")
|
||||
else:
|
||||
return results
|
||||
|
||||
def fetch(self, series):
|
||||
if len(series.base) != 3 or len(series.quote) != 3:
|
||||
raise exceptions.InvalidPair(series.base, series.quote, self)
|
||||
|
||||
series_name = f"FX{series.base}{series.quote}"
|
||||
data = self._data(series, series_name)
|
||||
|
||||
prices = []
|
||||
for o in data.get("observations", []):
|
||||
prices.append(Price(o["d"], Decimal(o[series_name]["v"])))
|
||||
|
||||
return dataclasses.replace(series, prices=prices)
|
||||
|
||||
def _data(self, series, series_name):
|
||||
url = f"https://www.bankofcanada.ca/valet/observations/{series_name}/json"
|
||||
params = {
|
||||
"start_date": series.start,
|
||||
"end_date": series.end,
|
||||
"order_dir": "asc",
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.log_curl(requests.get(url, params=params))
|
||||
except Exception as e:
|
||||
raise exceptions.RequestError(str(e)) from e
|
||||
|
||||
code = response.status_code
|
||||
text = response.text
|
||||
|
||||
try:
|
||||
result = json.loads(response.content)
|
||||
except Exception as e:
|
||||
raise exceptions.ResponseParsingError(str(e)) from e
|
||||
|
||||
if code == 404 and "not found" in text:
|
||||
raise exceptions.InvalidPair(series.base, series.quote, self)
|
||||
elif code == 400 and "End date must be greater than the Start date" in text:
|
||||
raise exceptions.BadResponse(result["message"])
|
||||
else:
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
raise exceptions.BadResponse(str(e)) from e
|
||||
|
||||
return result
|
|
@ -80,6 +80,18 @@ date,base,quote,amount,source,type
|
|||
END
|
||||
run_test "$name" "$cmd" "$expected"
|
||||
|
||||
name="Bank of Canada"
|
||||
cmd="pricehist fetch bankofcanada CAD/USD -s 2021-01-04 -e 2021-01-08"
|
||||
read -r -d '' expected <<END
|
||||
date,base,quote,amount,source,type
|
||||
2021-01-04,CAD,USD,0.7843,bankofcanada,default
|
||||
2021-01-05,CAD,USD,0.7870,bankofcanada,default
|
||||
2021-01-06,CAD,USD,0.7883,bankofcanada,default
|
||||
2021-01-07,CAD,USD,0.7870,bankofcanada,default
|
||||
2021-01-08,CAD,USD,0.7871,bankofcanada,default
|
||||
END
|
||||
run_test "$name" "$cmd" "$expected"
|
||||
|
||||
name="Coinbase Pro"
|
||||
cmd="pricehist fetch coinbasepro BTC/EUR -s 2021-01-04 -e 2021-01-08"
|
||||
read -r -d '' expected <<END
|
||||
|
|
246
tests/pricehist/sources/test_bankofcanada.py
Normal file
246
tests/pricehist/sources/test_bankofcanada.py
Normal file
|
@ -0,0 +1,246 @@
|
|||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import responses
|
||||
|
||||
from pricehist import exceptions
|
||||
from pricehist.price import Price
|
||||
from pricehist.series import Series
|
||||
from pricehist.sources.bankofcanada import BankOfCanada
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def src():
|
||||
return BankOfCanada()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def type(src):
|
||||
return src.types()[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def requests_mock():
|
||||
with responses.RequestsMock() as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def series_list_url():
|
||||
return "https://www.bankofcanada.ca/valet/lists/series/json"
|
||||
|
||||
|
||||
def fetch_url(series_name):
|
||||
return f"https://www.bankofcanada.ca/valet/observations/{series_name}/json"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def series_list_json():
|
||||
dir = Path(os.path.splitext(__file__)[0])
|
||||
return (dir / "series-partial.json").read_text()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def series_list_response_ok(requests_mock, series_list_url, series_list_json):
|
||||
requests_mock.add(responses.GET, series_list_url, body=series_list_json, status=200)
|
||||
yield requests_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recent_response_ok(requests_mock):
|
||||
json = (Path(os.path.splitext(__file__)[0]) / "recent.json").read_text()
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body=json, status=200)
|
||||
yield requests_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def all_response_ok(requests_mock):
|
||||
json = (Path(os.path.splitext(__file__)[0]) / "all-partial.json").read_text()
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body=json, status=200)
|
||||
yield requests_mock
|
||||
|
||||
|
||||
def test_normalizesymbol(src):
|
||||
assert src.normalizesymbol("cad") == "CAD"
|
||||
assert src.normalizesymbol("usd") == "USD"
|
||||
|
||||
|
||||
def test_metadata(src):
|
||||
assert isinstance(src.id(), str)
|
||||
assert len(src.id()) > 0
|
||||
|
||||
assert isinstance(src.name(), str)
|
||||
assert len(src.name()) > 0
|
||||
|
||||
assert isinstance(src.description(), str)
|
||||
assert len(src.description()) > 0
|
||||
|
||||
assert isinstance(src.source_url(), str)
|
||||
assert src.source_url().startswith("http")
|
||||
|
||||
assert datetime.strptime(src.start(), "%Y-%m-%d")
|
||||
|
||||
assert isinstance(src.types(), list)
|
||||
assert len(src.types()) > 0
|
||||
assert isinstance(src.types()[0], str)
|
||||
assert len(src.types()[0]) > 0
|
||||
|
||||
assert isinstance(src.notes(), str)
|
||||
|
||||
|
||||
def test_symbols(src, series_list_response_ok):
|
||||
syms = src.symbols()
|
||||
assert ("CAD/USD", "Canadian dollar to US dollar daily exchange rate") in syms
|
||||
assert len(syms) > 3
|
||||
|
||||
|
||||
def test_symbols_requests_logged(src, series_list_response_ok, caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
src.symbols()
|
||||
assert any(
|
||||
["DEBUG" == r.levelname and "curl " in r.message for r in caplog.records]
|
||||
)
|
||||
|
||||
|
||||
def test_symbols_not_found(src, requests_mock, series_list_url):
|
||||
requests_mock.add(responses.GET, series_list_url, body='{"series":{}}', status=200)
|
||||
with pytest.raises(exceptions.ResponseParsingError) as e:
|
||||
src.symbols()
|
||||
assert "data not found" in str(e.value)
|
||||
|
||||
|
||||
def test_symbols_network_issue(src, requests_mock, series_list_url):
|
||||
requests_mock.add(
|
||||
responses.GET,
|
||||
series_list_url,
|
||||
body=requests.exceptions.ConnectionError("Network issue"),
|
||||
)
|
||||
with pytest.raises(exceptions.RequestError) as e:
|
||||
src.symbols()
|
||||
assert "Network issue" in str(e.value)
|
||||
|
||||
|
||||
def test_symbols_bad_status(src, requests_mock, series_list_url):
|
||||
requests_mock.add(responses.GET, series_list_url, status=500)
|
||||
with pytest.raises(exceptions.BadResponse) as e:
|
||||
src.symbols()
|
||||
assert "Server Error" in str(e.value)
|
||||
|
||||
|
||||
def test_symbols_parsing_error(src, requests_mock, series_list_url):
|
||||
requests_mock.add(responses.GET, series_list_url, body="NOT JSON")
|
||||
with pytest.raises(exceptions.ResponseParsingError) as e:
|
||||
src.symbols()
|
||||
assert "while parsing data" in str(e.value)
|
||||
|
||||
|
||||
def test_fetch_known_pair(src, type, recent_response_ok):
|
||||
series = src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07"))
|
||||
req = recent_response_ok.calls[0].request
|
||||
assert req.params["order_dir"] == "asc"
|
||||
assert req.params["start_date"] == "2021-01-01"
|
||||
assert req.params["end_date"] == "2021-01-07"
|
||||
assert series.prices[0] == Price("2021-01-04", Decimal("0.7843"))
|
||||
assert series.prices[-1] == Price("2021-01-07", Decimal("0.7870"))
|
||||
assert len(series.prices) == 4
|
||||
|
||||
|
||||
def test_fetch_requests_logged(src, type, recent_response_ok, caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07"))
|
||||
assert any(
|
||||
["DEBUG" == r.levelname and "curl " in r.message for r in caplog.records]
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_long_hist_from_start(src, type, all_response_ok):
|
||||
series = src.fetch(Series("CAD", "USD", type, src.start(), "2021-01-07"))
|
||||
assert series.prices[0] == Price("2017-01-03", Decimal("0.7443"))
|
||||
assert series.prices[-1] == Price("2021-01-07", Decimal("0.7870"))
|
||||
assert len(series.prices) > 13
|
||||
|
||||
|
||||
def test_fetch_from_before_start(src, type, requests_mock):
|
||||
body = """{ "observations": [] }"""
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=200, body=body)
|
||||
series = src.fetch(Series("CAD", "USD", type, "2000-01-01", "2017-01-01"))
|
||||
assert len(series.prices) == 0
|
||||
|
||||
|
||||
def test_fetch_to_future(src, type, all_response_ok):
|
||||
series = src.fetch(Series("CAD", "USD", type, "2021-01-01", "2100-01-01"))
|
||||
assert len(series.prices) > 0
|
||||
|
||||
|
||||
def test_wrong_dates_order(src, type, requests_mock):
|
||||
body = """{ "message": "The End date must be greater than the Start date." }"""
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=400, body=body)
|
||||
with pytest.raises(exceptions.BadResponse) as e:
|
||||
src.fetch(Series("CAD", "USD", type, "2021-01-07", "2021-01-01"))
|
||||
assert "End date must be greater" in str(e.value)
|
||||
|
||||
|
||||
def test_fetch_in_future(src, type, requests_mock):
|
||||
body = """{ "observations": [] }"""
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), status=200, body=body)
|
||||
series = src.fetch(Series("CAD", "USD", type, "2030-01-01", "2030-01-07"))
|
||||
assert len(series.prices) == 0
|
||||
|
||||
|
||||
def test_fetch_empty(src, type, requests_mock):
|
||||
requests_mock.add(
|
||||
responses.GET, fetch_url("FXCADUSD"), body="""{"observations":{}}"""
|
||||
)
|
||||
series = src.fetch(Series("CAD", "USD", type, "2021-01-03", "2021-01-03"))
|
||||
assert len(series.prices) == 0
|
||||
|
||||
|
||||
def test_fetch_no_quote(src, type):
|
||||
with pytest.raises(exceptions.InvalidPair):
|
||||
src.fetch(Series("CAD", "", type, "2021-01-01", "2021-01-07"))
|
||||
|
||||
|
||||
def test_fetch_unknown_pair(src, type, requests_mock):
|
||||
requests_mock.add(
|
||||
responses.GET,
|
||||
fetch_url("FXCADAFN"),
|
||||
status=404,
|
||||
body="""{
|
||||
"message": "Series FXCADAFN not found.",
|
||||
"docs": "https://www.bankofcanada.ca/valet/docs"
|
||||
}""",
|
||||
)
|
||||
with pytest.raises(exceptions.InvalidPair):
|
||||
src.fetch(Series("CAD", "AFN", type, "2021-01-01", "2021-01-07"))
|
||||
|
||||
|
||||
def test_fetch_network_issue(src, type, requests_mock):
|
||||
body = requests.exceptions.ConnectionError("Network issue")
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body=body)
|
||||
with pytest.raises(exceptions.RequestError) as e:
|
||||
src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07"))
|
||||
assert "Network issue" in str(e.value)
|
||||
|
||||
|
||||
def test_fetch_bad_status(src, type, requests_mock):
|
||||
requests_mock.add(
|
||||
responses.GET,
|
||||
fetch_url("FXCADUSD"),
|
||||
status=500,
|
||||
body="""{"message": "Some other reason"}""",
|
||||
)
|
||||
with pytest.raises(exceptions.BadResponse) as e:
|
||||
src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07"))
|
||||
assert "Internal Server Error" in str(e.value)
|
||||
|
||||
|
||||
def test_fetch_parsing_error(src, type, requests_mock):
|
||||
requests_mock.add(responses.GET, fetch_url("FXCADUSD"), body="NOT JSON")
|
||||
with pytest.raises(exceptions.ResponseParsingError) as e:
|
||||
src.fetch(Series("CAD", "USD", type, "2021-01-01", "2021-01-07"))
|
||||
assert "while parsing data" in str(e.value)
|
101
tests/pricehist/sources/test_bankofcanada/all-partial.json
Normal file
101
tests/pricehist/sources/test_bankofcanada/all-partial.json
Normal file
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"terms": {
|
||||
"url": "https://www.bankofcanada.ca/terms/"
|
||||
},
|
||||
"seriesDetail": {
|
||||
"FXCADUSD": {
|
||||
"label": "CAD/USD",
|
||||
"description": "Canadian dollar to US dollar daily exchange rate",
|
||||
"dimension": {
|
||||
"key": "d",
|
||||
"name": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"observations": [
|
||||
{
|
||||
"d": "2017-01-03",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7443"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-04",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7510"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-05",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7551"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-06",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7568"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-09",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7553"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-10",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7568"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2017-01-11",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7547"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2020-12-29",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7809"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2020-12-30",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7831"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2020-12-31",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7854"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-04",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7843"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-05",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7870"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-06",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7883"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-07",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7870"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
41
tests/pricehist/sources/test_bankofcanada/recent.json
Normal file
41
tests/pricehist/sources/test_bankofcanada/recent.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"terms": {
|
||||
"url": "https://www.bankofcanada.ca/terms/"
|
||||
},
|
||||
"seriesDetail": {
|
||||
"FXCADUSD": {
|
||||
"label": "CAD/USD",
|
||||
"description": "Canadian dollar to US dollar daily exchange rate",
|
||||
"dimension": {
|
||||
"key": "d",
|
||||
"name": "date"
|
||||
}
|
||||
}
|
||||
},
|
||||
"observations": [
|
||||
{
|
||||
"d": "2021-01-04",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7843"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-05",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7870"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-06",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7883"
|
||||
}
|
||||
},
|
||||
{
|
||||
"d": "2021-01-07",
|
||||
"FXCADUSD": {
|
||||
"v": "0.7870"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
272
tests/pricehist/sources/test_bankofcanada/series-partial.json
Normal file
272
tests/pricehist/sources/test_bankofcanada/series-partial.json
Normal file
|
@ -0,0 +1,272 @@
|
|||
{
|
||||
"terms": {
|
||||
"url": "https://www.bankofcanada.ca/terms/"
|
||||
},
|
||||
"series": {
|
||||
"FXAUDCAD": {
|
||||
"label": "AUD/CAD",
|
||||
"description": "Australian dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXAUDCAD"
|
||||
},
|
||||
"FXBRLCAD": {
|
||||
"label": "BRL/CAD",
|
||||
"description": "Brazilian real to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXBRLCAD"
|
||||
},
|
||||
"FXCNYCAD": {
|
||||
"label": "CNY/CAD",
|
||||
"description": "Chinese renminbi to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCNYCAD"
|
||||
},
|
||||
"FXEURCAD": {
|
||||
"label": "EUR/CAD",
|
||||
"description": "European euro to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXEURCAD"
|
||||
},
|
||||
"FXHKDCAD": {
|
||||
"label": "HKD/CAD",
|
||||
"description": "Hong Kong dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXHKDCAD"
|
||||
},
|
||||
"FXINRCAD": {
|
||||
"label": "INR/CAD",
|
||||
"description": "Indian rupee to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXINRCAD"
|
||||
},
|
||||
"FXIDRCAD": {
|
||||
"label": "IDR/CAD",
|
||||
"description": "Indonesian rupiah to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXIDRCAD"
|
||||
},
|
||||
"FXJPYCAD": {
|
||||
"label": "JPY/CAD",
|
||||
"description": "Japanese yen to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXJPYCAD"
|
||||
},
|
||||
"FXMYRCAD": {
|
||||
"label": "MYR/CAD",
|
||||
"description": "Malaysian ringgit to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXMYRCAD"
|
||||
},
|
||||
"FXMXNCAD": {
|
||||
"label": "MXN/CAD",
|
||||
"description": "Mexican peso to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXMXNCAD"
|
||||
},
|
||||
"FXNZDCAD": {
|
||||
"label": "NZD/CAD",
|
||||
"description": "New Zealand dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXNZDCAD"
|
||||
},
|
||||
"FXNOKCAD": {
|
||||
"label": "NOK/CAD",
|
||||
"description": "Norwegian krone to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXNOKCAD"
|
||||
},
|
||||
"FXPENCAD": {
|
||||
"label": "PEN/CAD",
|
||||
"description": "Peruvian new sol to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXPENCAD"
|
||||
},
|
||||
"FXRUBCAD": {
|
||||
"label": "RUB/CAD",
|
||||
"description": "Russian ruble to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXRUBCAD"
|
||||
},
|
||||
"FXSARCAD": {
|
||||
"label": "SAR/CAD",
|
||||
"description": "Saudi riyal to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXSARCAD"
|
||||
},
|
||||
"FXSGDCAD": {
|
||||
"label": "SGD/CAD",
|
||||
"description": "Singapore dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXSGDCAD"
|
||||
},
|
||||
"FXZARCAD": {
|
||||
"label": "ZAR/CAD",
|
||||
"description": "South African rand to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXZARCAD"
|
||||
},
|
||||
"FXKRWCAD": {
|
||||
"label": "KRW/CAD",
|
||||
"description": "South Korean won to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXKRWCAD"
|
||||
},
|
||||
"FXSEKCAD": {
|
||||
"label": "SEK/CAD",
|
||||
"description": "Swedish krona to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXSEKCAD"
|
||||
},
|
||||
"FXCHFCAD": {
|
||||
"label": "CHF/CAD",
|
||||
"description": "Swiss franc to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCHFCAD"
|
||||
},
|
||||
"FXTWDCAD": {
|
||||
"label": "TWD/CAD",
|
||||
"description": "Taiwanese dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXTWDCAD"
|
||||
},
|
||||
"FXTHBCAD": {
|
||||
"label": "THB/CAD",
|
||||
"description": "Thai baht to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXTHBCAD"
|
||||
},
|
||||
"FXTRYCAD": {
|
||||
"label": "TRY/CAD",
|
||||
"description": "Turkish lira to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXTRYCAD"
|
||||
},
|
||||
"FXGBPCAD": {
|
||||
"label": "GBP/CAD",
|
||||
"description": "UK pound sterling to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXGBPCAD"
|
||||
},
|
||||
"FXUSDCAD": {
|
||||
"label": "USD/CAD",
|
||||
"description": "US dollar to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXUSDCAD"
|
||||
},
|
||||
"FXVNDCAD": {
|
||||
"label": "VND/CAD",
|
||||
"description": "Vietnamese dong to Canadian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXVNDCAD"
|
||||
},
|
||||
"FXCADAUD": {
|
||||
"label": "CAD/AUD",
|
||||
"description": "Canadian dollar to Australian dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADAUD"
|
||||
},
|
||||
"FXCADBRL": {
|
||||
"label": "CAD/BRL",
|
||||
"description": "Canadian dollar to Brazilian real daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADBRL"
|
||||
},
|
||||
"FXCADCNY": {
|
||||
"label": "CAD/CNY",
|
||||
"description": "Canadian dollar to Chinese renminbi daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADCNY"
|
||||
},
|
||||
"FXCADEUR": {
|
||||
"label": "CAD/EUR",
|
||||
"description": "Canadian dollar to European euro daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADEUR"
|
||||
},
|
||||
"FXCADHKD": {
|
||||
"label": "CAD/HKD",
|
||||
"description": "Canadian dollar to Hong Kong dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADHKD"
|
||||
},
|
||||
"FXCADINR": {
|
||||
"label": "CAD/INR",
|
||||
"description": "Canadian dollar to Indian rupee daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADINR"
|
||||
},
|
||||
"FXCADIDR": {
|
||||
"label": "CAD/IDR",
|
||||
"description": "Canadian dollar to Indonesian rupiah daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADIDR"
|
||||
},
|
||||
"FXCADJPY": {
|
||||
"label": "CAD/JPY",
|
||||
"description": "Canadian dollar to Japanese yen daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADJPY"
|
||||
},
|
||||
"FXCADMYR": {
|
||||
"label": "CAD/MYR",
|
||||
"description": "Canadian dollar to Malaysian ringgit daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADMYR"
|
||||
},
|
||||
"FXCADMXN": {
|
||||
"label": "CAD/MXN",
|
||||
"description": "Canadian dollar to Mexican peso daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADMXN"
|
||||
},
|
||||
"FXCADNZD": {
|
||||
"label": "CAD/NZD",
|
||||
"description": "Canadian dollar to New Zealand dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADNZD"
|
||||
},
|
||||
"FXCADNOK": {
|
||||
"label": "CAD/NOK",
|
||||
"description": "Canadian dollar to Norwegian krone daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADNOK"
|
||||
},
|
||||
"FXCADPEN": {
|
||||
"label": "CAD/PEN",
|
||||
"description": "Canadian dollar to Peruvian new sol daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADPEN"
|
||||
},
|
||||
"FXCADRUB": {
|
||||
"label": "CAD/RUB",
|
||||
"description": "Canadian dollar to Russian ruble daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADRUB"
|
||||
},
|
||||
"FXCADSAR": {
|
||||
"label": "CAD/SAR",
|
||||
"description": "Canadian dollar to Saudi riyal daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADSAR"
|
||||
},
|
||||
"FXCADSGD": {
|
||||
"label": "CAD/SGD",
|
||||
"description": "Canadian dollar to Singapore dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADSGD"
|
||||
},
|
||||
"FXCADZAR": {
|
||||
"label": "CAD/ZAR",
|
||||
"description": "Canadian dollar to South African rand daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADZAR"
|
||||
},
|
||||
"FXCADKRW": {
|
||||
"label": "CAD/KRW",
|
||||
"description": "Canadian dollar to South Korean won daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADKRW"
|
||||
},
|
||||
"FXCADSEK": {
|
||||
"label": "CAD/SEK",
|
||||
"description": "Canadian dollar to Swedish krona daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADSEK"
|
||||
},
|
||||
"FXCADCHF": {
|
||||
"label": "CAD/CHF",
|
||||
"description": "Canadian dollar to Swiss franc daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADCHF"
|
||||
},
|
||||
"FXCADTWD": {
|
||||
"label": "CAD/TWD",
|
||||
"description": "Canadian dollar to Taiwanese dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADTWD"
|
||||
},
|
||||
"FXCADTHB": {
|
||||
"label": "CAD/THB",
|
||||
"description": "Canadian dollar to Thai baht daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADTHB"
|
||||
},
|
||||
"FXCADTRY": {
|
||||
"label": "CAD/TRY",
|
||||
"description": "Canadian dollar to Turkish lira daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADTRY"
|
||||
},
|
||||
"FXCADGBP": {
|
||||
"label": "CAD/GBP",
|
||||
"description": "Canadian dollar to UK pound sterling daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADGBP"
|
||||
},
|
||||
"FXCADUSD": {
|
||||
"label": "CAD/USD",
|
||||
"description": "Canadian dollar to US dollar daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADUSD"
|
||||
},
|
||||
"FXCADVND": {
|
||||
"label": "CAD/VND",
|
||||
"description": "Canadian dollar to Vietnamese dong daily exchange rate",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/FXCADVND"
|
||||
},
|
||||
"INDINF_GRACE_Q": {
|
||||
"label": "Foreign demand for Canadian non-commodity exports (GRACE) (2007=100)",
|
||||
"description": "Foreign demand for Canadian non-commodity exports (GRACE) (2007=100)",
|
||||
"link": "https://www.bankofcanada.ca/valet/series/INDINF_GRACE_Q"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue