├── .coveragerc ├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── ccy ├── __init__.py ├── cli │ ├── __init__.py │ └── console.py ├── core │ ├── __init__.py │ ├── country.py │ ├── currency.py │ ├── data.py │ └── daycounter.py ├── dates │ ├── __init__.py │ ├── converters.py │ ├── futures.py │ ├── period.py │ └── utils.py ├── py.typed └── tradingcentres │ └── __init__.py ├── dev ├── install ├── lint └── start-jupyter ├── docs ├── _config.yml ├── _toc.yml ├── dates.md └── intro.md ├── poetry.lock ├── pyproject.toml ├── readme.md └── tests ├── __init__.py ├── test_ccy.py ├── test_dates.py ├── test_dc.py └── test_tcs.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ccy 3 | data_file = build/.coverage 4 | 5 | [html] 6 | directory = build/coverage/html 7 | 8 | [xml] 9 | output = build/coverage.xml 10 | 11 | [report] 12 | exclude_lines = 13 | pragma: no cover 14 | raise NotImplementedError 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .env 3 | .pytest_cache 4 | .mypy_cache 5 | .github 6 | .vscode 7 | .gitignore 8 | .dockerignore 9 | *.egg-info 10 | *.pyc 11 | venv 12 | build 13 | dist 14 | dev/Dockerfile 15 | __pycache__ 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - deploy 7 | tags-ignore: 8 | - v* 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | env: 15 | PYTHON_ENV: ci 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 18 | strategy: 19 | matrix: 20 | python-version: ["3.10", "3.11", "3.12", "3.13"] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install poetry 29 | run: pip install -U pip poetry 30 | - name: Install dependencies 31 | run: poetry install --all-extras 32 | - name: run lint 33 | run: make lint 34 | - name: run tests 35 | run: make test 36 | - name: upload coverage reports to codecov 37 | if: matrix.python-version == '3.12' 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | files: ./build/coverage.xml 42 | 43 | book: 44 | runs-on: ubuntu-latest 45 | env: 46 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 47 | needs: 48 | - build 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Set up Python 3.12 52 | uses: actions/setup-python@v5 53 | with: 54 | python-version: "3.12" 55 | - name: Install all dev dependencies 56 | run: make install-dev 57 | - name: build book 58 | run: make book 59 | - name: publish book 60 | run: make publish-book 61 | - name: publish 62 | if: ${{ github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release' }} 63 | run: make publish 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #mac 2 | .DS_Store 3 | 4 | #IDE 5 | .idea 6 | .vscode 7 | 8 | #python 9 | *.pyc 10 | *.log 11 | __pycache__ 12 | .eggs 13 | _build 14 | ccy.egg-info 15 | build 16 | docs/build 17 | htmlcov 18 | dist 19 | venv 20 | MANIFEST 21 | .python-version 22 | .settings 23 | .coverage 24 | .env 25 | 26 | *.ipynb 27 | .ipynb_checkpoints 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2023, Quantmind 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the names of its contributors 13 | may be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: help 3 | help: 4 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' 5 | 6 | .PHONY: clean 7 | clean: ## Remove python cache files 8 | find . -name '__pycache__' | xargs rm -rf 9 | find . -name '*.pyc' -delete 10 | rm -rf build 11 | rm -rf dist 12 | rm -rf .pytest_cache 13 | rm -rf .coverage 14 | 15 | .PHONY: docs 16 | docs: ## Build docs 17 | cd docs && make docs 18 | 19 | .PHONY: install-dev 20 | install-dev: ## Install packages for development 21 | @./dev/install 22 | 23 | .PHONY: lint 24 | lint: ## Run linters 25 | @poetry run ./dev/lint fix 26 | 27 | .PHONY: lint-check 28 | lint-check: ## Run linters in check mode 29 | @poetry run ./dev/lint 30 | 31 | .PHONY: test 32 | test: ## Test with python 3.8 with coverage 33 | @poetry run pytest -x -v --cov --cov-report xml 34 | 35 | .PHONY: publish 36 | publish: ## Release to pypi 37 | @poetry publish --build -u __token__ -p $(PYPI_TOKEN) 38 | 39 | .PHONY: notebook 40 | notebook: ## Run Jupyter notebook server 41 | @poetry run ./dev/start-jupyter 9095 42 | 43 | .PHONY: book 44 | book: ## Build static jupyter {book} 45 | poetry run jupyter-book build docs --all 46 | 47 | .PHONY: publish-book 48 | publish-book: ## Publish the book to github pages 49 | poetry run ghp-import -n -p -f docs/_build/html 50 | 51 | .PHONY: outdated 52 | outdated: ## Show outdated packages 53 | poetry show -o -a 54 | -------------------------------------------------------------------------------- /ccy/__init__.py: -------------------------------------------------------------------------------- 1 | """Python currencies""" 2 | 3 | __version__ = "1.7.1" 4 | 5 | 6 | from .core.country import ( 7 | CountryError, 8 | countries, 9 | country, 10 | country_map, 11 | countryccy, 12 | eurozone, 13 | print_eurozone, 14 | set_new_country, 15 | ) 16 | from .core.currency import ( 17 | ccypair, 18 | currency, 19 | currency_pair, 20 | currencydb, 21 | dump_currency_table, 22 | ) 23 | from .core.daycounter import alldc, getdc 24 | from .dates.converters import ( 25 | date2juldate, 26 | date2timestamp, 27 | date2yyyymmdd, 28 | date_from_string, 29 | jstimestamp, 30 | juldate2date, 31 | timestamp2date, 32 | todate, 33 | yyyymmdd2date, 34 | ) 35 | from .dates.futures import future_date_to_code, future_month_dict 36 | from .dates.period import Period, period 37 | 38 | __all__ = [ 39 | "currency", 40 | "currencydb", 41 | "ccypair", 42 | "currency_pair", 43 | "dump_currency_table", 44 | # 45 | "getdc", 46 | "alldc", 47 | # 48 | "country", 49 | "countryccy", 50 | "set_new_country", 51 | "countries", 52 | "country_map", 53 | "CountryError", 54 | "eurozone", 55 | "print_eurozone", 56 | "future_date_to_code", 57 | "future_month_dict", 58 | "period", 59 | "Period", 60 | "todate", 61 | "date2timestamp", 62 | "timestamp2date", 63 | "yyyymmdd2date", 64 | "date2yyyymmdd", 65 | "juldate2date", 66 | "date2juldate", 67 | "date_from_string", 68 | "jstimestamp", 69 | ] 70 | 71 | 72 | # Shortcuts 73 | def cross(code: str) -> str: 74 | return currency(code).as_cross() 75 | 76 | 77 | def crossover(code: str) -> str: 78 | return currency(code).as_cross("/") 79 | 80 | 81 | def all() -> tuple[str, ...]: 82 | return tuple(currencydb()) 83 | 84 | 85 | def g7() -> tuple[str, ...]: 86 | return ("EUR", "GBP", "USD", "CAD") 87 | 88 | 89 | def g10() -> tuple[str, ...]: 90 | return g7() + ("CHF", "SEK", "JPY") 91 | 92 | 93 | def g10m() -> tuple[str, ...]: 94 | """modified g10 = G10 + AUD, NZD, NOK""" 95 | return g10() + ("AUD", "NZD", "NOK") 96 | -------------------------------------------------------------------------------- /ccy/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pandas as pd 3 | from rich.console import Console 4 | 5 | import ccy 6 | 7 | from .console import df_to_rich 8 | 9 | 10 | @click.group() 11 | def ccys() -> None: 12 | """Currency commands.""" 13 | 14 | 15 | @ccys.command() 16 | def show() -> None: 17 | """Show table with all currencies.""" 18 | df = pd.DataFrame(ccy.dump_currency_table()) 19 | console = Console() 20 | console.print(df_to_rich(df, exclude=("symbol_raw",))) 21 | -------------------------------------------------------------------------------- /ccy/cli/console.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from rich.table import Table 3 | 4 | set_alike = set[str] | frozenset[str] | tuple[str] | list[str] | None 5 | 6 | 7 | def df_to_rich( 8 | df: pd.DataFrame, *, exclude: set_alike = None, **columns: dict 9 | ) -> Table: 10 | table = Table() 11 | if exclude_columns := set(exclude or ()): 12 | df = df.drop(columns=exclude_columns) 13 | for column in df.columns: 14 | config = dict(justify="right", style="cyan", no_wrap=True) 15 | config.update(columns.get(column) or {}) 16 | table.add_column(column, **config) # type: ignore[arg-type] 17 | for row in df.values: 18 | table.add_row(*[str(item) for item in row]) 19 | return table 20 | -------------------------------------------------------------------------------- /ccy/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/ccy/a322e3730f9ec1fbda86d9614254013efa05c867/ccy/core/__init__.py -------------------------------------------------------------------------------- /ccy/core/country.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Protocol 4 | 5 | from .currency import currencydb 6 | 7 | if TYPE_CHECKING: 8 | pass 9 | 10 | # Eurozone countries (officially the euro area) 11 | # see http://en.wikipedia.org/wiki/Eurozone 12 | # using ISO 3166-1 alpha-2 country codes 13 | # see http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 14 | # 15 | eurozone = tuple( 16 | ("AT BE CY DE EE ES FI FR GR HR IE IT LU LV LT MT NL PT SI SK").split(" ") 17 | ) 18 | 19 | 20 | def print_eurozone() -> None: 21 | for c in sorted(country(c).name for c in eurozone): 22 | print(c) 23 | 24 | 25 | _countries: dict[str, Country] = {} 26 | _country_ccys: dict[str, str] = {} 27 | _country_maps: dict[str, str] = {} 28 | 29 | 30 | class Country(Protocol): 31 | alpha_2: str 32 | name: str 33 | alpha_3: str = "" 34 | 35 | 36 | class CountryError(Exception): 37 | pass 38 | 39 | 40 | def country(code: str) -> Country: 41 | cdb = countries() 42 | code = country_map(code) 43 | return cdb[code] 44 | 45 | 46 | def countryccy(code: str) -> str: 47 | cdb = countryccys() 48 | code = str(code).upper() 49 | return cdb.get(code, "") 50 | 51 | 52 | def countries() -> dict[str, Country]: 53 | """ 54 | get country dictionary from pytz and add some extra. 55 | """ 56 | global _countries 57 | if not _countries: 58 | try: 59 | import pycountry 60 | 61 | _countries = {country.alpha_2: country for country in pycountry.countries} 62 | except Exception: 63 | pass 64 | return _countries 65 | 66 | 67 | def countryccys() -> dict[str, str]: 68 | """ 69 | Create a dictionary with keys given by countries ISO codes and values 70 | given by their currencies 71 | """ 72 | global _country_ccys 73 | if not _country_ccys: 74 | v: dict[str, str] = {} 75 | _country_ccys = v 76 | ccys = currencydb() 77 | for euc in eurozone: 78 | v[euc] = "EUR" 79 | for c in ccys.values(): 80 | if c.default_country: 81 | v[c.default_country] = c.code 82 | return _country_ccys 83 | 84 | 85 | def set_new_country(code: str, ccy: str, name: str) -> None: 86 | """ 87 | Add new country code to database 88 | """ 89 | code = str(code).upper() 90 | cdb = countries() 91 | if code in cdb: 92 | raise CountryError("Country %s already in database" % code) 93 | ccys = currencydb() 94 | ccy = str(ccy).upper() 95 | if ccy not in ccys: 96 | raise CountryError("Currency %s not in database" % ccy) 97 | # hacky - but best way I could find 98 | cdb[code] = type(cdb["IT"])( 99 | alpha_2=code, 100 | name=name, 101 | official_name=name, 102 | ) # type: ignore 103 | cccys = countryccys() 104 | cccys[code] = ccy 105 | 106 | 107 | def country_map(code: str) -> str: 108 | """ 109 | Country mapping 110 | """ 111 | code = str(code).upper() 112 | global _country_maps 113 | return _country_maps.get(code, code) 114 | 115 | 116 | # Add eurozone to list of Countries 117 | set_new_country("EZ", "EUR", "Eurozone") 118 | # lagacy - to remove 119 | set_new_country("EU", "EUR", "Eurozone") 120 | -------------------------------------------------------------------------------- /ccy/core/currency.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Any, Callable, NamedTuple 5 | 6 | from .data import make_ccys 7 | 8 | usd_order = 5 9 | 10 | 11 | def to_string(v: Any) -> str: 12 | if isinstance(v, bytes): 13 | return v.decode("utf-8") 14 | else: 15 | return "%s" % v 16 | 17 | 18 | def overusdfun(v1: float) -> float: 19 | return v1 20 | 21 | 22 | def overusdfuni(v1: float) -> float: 23 | return 1.0 / v1 24 | 25 | 26 | class ccy(NamedTuple): 27 | code: str 28 | isonumber: str 29 | twoletterscode: str 30 | order: int 31 | name: str 32 | rounding: int 33 | default_country: str 34 | fixeddc: str = "Act/365" 35 | floatdc: str = "Act/365" 36 | fixedfreq: str = "" 37 | floatfreq: str = "" 38 | future: str = "" 39 | symbol_raw: str = r"\00a4" 40 | html: str = "" 41 | 42 | @property 43 | def symbol(self) -> str: 44 | return self.symbol_raw.encode("utf-8").decode("unicode_escape") 45 | 46 | def __eq__(self, other: Any) -> bool: 47 | if isinstance(other, ccy): 48 | return other.code == self.code 49 | return False 50 | 51 | def description(self) -> str: 52 | if self.order > usd_order: 53 | v = "USD / %s" % self.code 54 | else: 55 | v = "%s / USD" % self.code 56 | if self.order != usd_order: 57 | return "%s Spot Exchange Rate" % v 58 | else: 59 | return "Dollar" 60 | 61 | def info(self) -> dict[str, Any]: 62 | data = self._asdict() 63 | data["symbol"] = self.symbol 64 | return data 65 | 66 | def printinfo(self, stream: Any | None = None) -> None: 67 | info = self.info() 68 | stream = stream or sys.stdout 69 | for k, v in info.items(): 70 | stream.write(to_string("%s: %s\n" % (k, v))) 71 | 72 | def __str__(self) -> str: 73 | return self.code 74 | 75 | def swap(self, c2: ccy) -> tuple[bool, ccy, ccy]: 76 | """ 77 | put the order of currencies as market standard 78 | """ 79 | inv = False 80 | c1 = self 81 | if c1.order > c2.order: 82 | ct = c1 83 | c1 = c2 84 | c2 = ct 85 | inv = True 86 | return inv, c1, c2 87 | 88 | def overusdfunc(self) -> Callable[[float], float]: 89 | if self.order > usd_order: 90 | return overusdfuni 91 | else: 92 | return overusdfun 93 | 94 | def usdoverfunc(self) -> Callable[[float], float]: 95 | if self.order > usd_order: 96 | return overusdfun 97 | else: 98 | return overusdfuni 99 | 100 | def as_cross(self, delimiter: str = "") -> str: 101 | """ 102 | Return a cross rate representation with respect USD. 103 | @param delimiter: could be '' or '/' normally 104 | """ 105 | if self.order > usd_order: 106 | return "USD%s%s" % (delimiter, self.code) 107 | else: 108 | return "%s%sUSD" % (self.code, delimiter) 109 | 110 | def spot(self, c2: ccy, v1: float, v2: float) -> float: 111 | if self.order > c2.order: 112 | vt = v1 113 | v1 = v2 114 | v2 = vt 115 | return v1 / v2 116 | 117 | 118 | class ccy_pair(NamedTuple): 119 | """ 120 | Currency pair such as EURUSD, USDCHF 121 | 122 | XXXYYY - XXX is the foreign currency, while YYY is the base currency 123 | 124 | XXXYYY means 1 unit of of XXX cost XXXYYY units of YYY 125 | """ 126 | 127 | ccy1: ccy 128 | ccy2: ccy 129 | 130 | @property 131 | def code(self) -> str: 132 | return "%s%s" % (self.ccy1, self.ccy2) 133 | 134 | def __repr__(self) -> str: 135 | return "%s: %s" % (self.__class__.__name__, self.code) 136 | 137 | def __str__(self) -> str: 138 | return self.code 139 | 140 | def mkt(self) -> ccy_pair: 141 | if self.ccy1.order > self.ccy2.order: 142 | return ccy_pair(self.ccy2, self.ccy1) 143 | else: 144 | return self 145 | 146 | def over(self, name: str = "usd") -> ccy_pair: 147 | """Returns a new currency pair with the *over* currency as 148 | second part of the pair (Foreign currency).""" 149 | name = name.upper() 150 | if self.ccy1.code == name.upper(): 151 | return ccy_pair(self.ccy2, self.ccy1) 152 | else: 153 | return self 154 | 155 | 156 | class ccydb(dict[str, ccy]): 157 | def insert(self, *args: Any, **kwargs: Any) -> None: 158 | c = ccy(*args, **kwargs) 159 | self[c.code] = c 160 | 161 | 162 | def currencydb() -> ccydb: 163 | global _ccys 164 | if not _ccys: 165 | _ccys = ccydb() 166 | make_ccys(_ccys) 167 | return _ccys 168 | 169 | 170 | def ccypairsdb() -> dict[str, ccy_pair]: 171 | global _ccypairs 172 | if not _ccypairs: 173 | _ccypairs = make_ccypairs() 174 | return _ccypairs 175 | 176 | 177 | def currency(code: str | ccy) -> ccy: 178 | c = currencydb() 179 | return c[str(code).upper()] 180 | 181 | 182 | def ccypair(code: str | ccy_pair) -> ccy_pair: 183 | c = ccypairsdb() 184 | return c[str(code).upper()] 185 | 186 | 187 | def currency_pair(code: str | ccy_pair) -> ccy_pair: 188 | """Construct a :class:`ccy_pair` from a six letter string.""" 189 | c = str(code) 190 | c1 = currency(c[:3]) 191 | c2 = currency(c[3:]) 192 | return ccy_pair(c1, c2) 193 | 194 | 195 | def make_ccypairs() -> dict[str, ccy_pair]: 196 | ccys = currencydb() 197 | db = {} 198 | 199 | for ccy1 in ccys.values(): 200 | od = ccy1.order 201 | for ccy2 in ccys.values(): 202 | if ccy2.order <= od: 203 | continue 204 | p = ccy_pair(ccy1, ccy2) 205 | db[p.code] = p 206 | return db 207 | 208 | 209 | def dump_currency_table() -> list: 210 | return [c.info() for c in sorted(currencydb().values(), key=lambda x: x.order)] 211 | 212 | 213 | _ccys: ccydb = ccydb() 214 | _ccypairs: dict[str, ccy_pair] = {} 215 | -------------------------------------------------------------------------------- /ccy/core/data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from .currency import ccydb 7 | 8 | 9 | def make_ccys(db: ccydb) -> None: 10 | """ 11 | Create the currency dictionary 12 | """ 13 | dfr = 4 14 | dollar = r"\u0024" 15 | peso = r"\u20b1" 16 | kr = r"kr" 17 | insert = db.insert 18 | 19 | # G10 & SCANDI 20 | insert( 21 | "EUR", 22 | "978", 23 | "EU", 24 | 1, 25 | "Euro", 26 | dfr, 27 | "EU", 28 | "30/360", 29 | "ACT/360", 30 | future="FE", 31 | symbol_raw=r"\u20ac", 32 | html="€", 33 | ) 34 | insert( 35 | "GBP", 36 | "826", 37 | "BP", 38 | 2, 39 | "British Pound", 40 | dfr, 41 | "GB", 42 | "ACT/365", 43 | "ACT/365", 44 | symbol_raw=r"\u00a3", 45 | html="£", 46 | ) 47 | insert( 48 | "AUD", 49 | "036", 50 | "AD", 51 | 3, 52 | "Australian Dollar", 53 | dfr, 54 | "AU", 55 | "ACT/365", 56 | "ACT/365", 57 | symbol_raw=dollar, 58 | html="$", 59 | ) 60 | insert( 61 | "NZD", 62 | "554", 63 | "ND", 64 | 4, 65 | "New-Zealand Dollar", 66 | dfr, 67 | "NZ", 68 | "ACT/365", 69 | "ACT/365", 70 | symbol_raw=dollar, 71 | html="$", 72 | ) 73 | insert( 74 | "USD", 75 | "840", 76 | "UD", 77 | 5, 78 | "US Dollar", 79 | 0, 80 | "US", 81 | "30/360", 82 | "ACT/360", 83 | future="ED", 84 | symbol_raw=dollar, 85 | html="$", 86 | ) 87 | insert( 88 | "CAD", 89 | "124", 90 | "CD", 91 | 6, 92 | "Canadian Dollar", 93 | dfr, 94 | "CA", 95 | "ACT/365", 96 | "ACT/365", 97 | symbol_raw=dollar, 98 | html="$", 99 | ) 100 | insert( 101 | "CHF", 102 | "756", 103 | "SF", 104 | 7, 105 | "Swiss Franc", 106 | dfr, 107 | "CH", 108 | "30/360", 109 | "ACT/360", 110 | symbol_raw=r"Fr", 111 | html="₣", 112 | ) 113 | insert( 114 | "NOK", 115 | "578", 116 | "NK", 117 | 8, 118 | "Norwegian Krona", 119 | dfr, 120 | "NO", 121 | "30/360", 122 | "ACT/360", 123 | symbol_raw=kr, 124 | html=kr, 125 | ) 126 | insert( 127 | "SEK", 128 | "752", 129 | "SK", 130 | 9, 131 | "Swedish Krona", 132 | dfr, 133 | "SE", 134 | "30/360", 135 | "ACT/360", 136 | symbol_raw=kr, 137 | html=kr, 138 | ) 139 | insert( 140 | "DKK", 141 | "208", 142 | "DK", 143 | 10, 144 | "Danish Krona", 145 | dfr, 146 | "DK", 147 | "30/360", 148 | "ACT/360", 149 | symbol_raw=kr, 150 | html=kr, 151 | ) 152 | insert( 153 | "JPY", 154 | "392", 155 | "JY", 156 | 10000, 157 | "Japanese Yen", 158 | 2, 159 | "JP", 160 | "ACT/365", 161 | "ACT/360", 162 | symbol_raw=r"\u00a5", 163 | html="¥", 164 | ) 165 | 166 | # ASIA 167 | insert( 168 | "CNY", 169 | "156", 170 | "CY", 171 | 680, 172 | "Chinese Renminbi", 173 | dfr, 174 | "CN", 175 | "ACT/365", 176 | "ACT/365", 177 | symbol_raw=r"\u00a5", 178 | html="¥", 179 | ) 180 | insert( 181 | "KRW", 182 | "410", 183 | "KW", 184 | 110000, 185 | "South Korean won", 186 | 2, 187 | "KR", 188 | "ACT/365", 189 | "ACT/365", 190 | symbol_raw=r"\u20a9", 191 | html="₩", 192 | ) 193 | insert( 194 | "SGD", 195 | "702", 196 | "SD", 197 | 15, 198 | "Singapore Dollar", 199 | dfr, 200 | "SG", 201 | "ACT/365", 202 | "ACT/365", 203 | symbol_raw=dollar, 204 | html="$", 205 | ) 206 | insert( 207 | "IDR", 208 | "360", 209 | "IH", 210 | 970000, 211 | "Indonesian Rupiah", 212 | 0, 213 | "ID", 214 | "ACT/360", 215 | "ACT/360", 216 | symbol_raw=r"Rp", 217 | html="Rp", 218 | ) 219 | insert( 220 | "THB", 221 | "764", 222 | "TB", 223 | 3300, 224 | "Thai Baht", 225 | 2, 226 | "TH", 227 | "ACT/365", 228 | "ACT/365", 229 | symbol_raw=r"\u0e3f", 230 | html="฿", 231 | ) 232 | insert( 233 | "TWD", 234 | "901", 235 | "TD", 236 | 18, 237 | "Taiwan Dollar", 238 | dfr, 239 | "TW", 240 | "ACT/365", 241 | "ACT/365", 242 | symbol_raw=dollar, 243 | html="$", 244 | ) 245 | insert( 246 | "HKD", 247 | "344", 248 | "HD", 249 | 19, 250 | "Hong Kong Dollar", 251 | dfr, 252 | "HK", 253 | "ACT/365", 254 | "ACT/365", 255 | symbol_raw=r"\u5713", 256 | html="HK$", 257 | ) 258 | insert( 259 | "PHP", 260 | "608", 261 | "PP", 262 | 4770, 263 | "Philippines Peso", 264 | dfr, 265 | "PH", 266 | "ACT/360", 267 | "ACT/360", 268 | symbol_raw=peso, 269 | html="₱", 270 | ) 271 | insert( 272 | "INR", 273 | "356", 274 | "IR", 275 | 4500, 276 | "Indian Rupee", 277 | dfr, 278 | "IN", 279 | "ACT/365", 280 | "ACT/365", 281 | symbol_raw=r"\u20a8", 282 | html="₨", 283 | ) 284 | insert( 285 | "MYR", "458", "MR", 345, "Malaysian Ringgit", dfr, "MY", "ACT/365", "ACT/365" 286 | ) 287 | insert( 288 | "VND", 289 | "704", 290 | "VD", 291 | 1700000, 292 | "Vietnamese Dong", 293 | 0, 294 | "VN", 295 | "ACT/365", 296 | "ACT/365", 297 | symbol_raw=r"\u20ab", 298 | html="₫", 299 | ) 300 | 301 | # LATIN AMERICA 302 | insert( 303 | "BRL", 304 | "986", 305 | "BC", 306 | 200, 307 | "Brazilian Real", 308 | dfr, 309 | "BR", 310 | "BUS/252", 311 | "BUS/252", 312 | symbol_raw=r"R$", 313 | ) 314 | insert( 315 | "PEN", 316 | "604", 317 | "PS", 318 | 220, 319 | "Peruvian New Sol", 320 | dfr, 321 | "PE", 322 | "ACT/360", 323 | "ACT/360", 324 | symbol_raw=r"S/.", 325 | ) 326 | insert( 327 | "ARS", 328 | "032", 329 | "AP", 330 | 301, 331 | "Argentine Peso", 332 | dfr, 333 | "AR", 334 | "30/360", 335 | "ACT/360", 336 | symbol_raw=dollar, 337 | html="$", 338 | ) 339 | insert( 340 | "MXN", 341 | "484", 342 | "MP", 343 | 1330, 344 | "Mexican Peso", 345 | dfr, 346 | "MX", 347 | "ACT/360", 348 | "ACT/360", 349 | symbol_raw=dollar, 350 | html="$", 351 | ) 352 | insert( 353 | "CLP", 354 | "152", 355 | "CH", 356 | 54500, 357 | "Chilean Peso", 358 | 2, 359 | "CL", 360 | "ACT/360", 361 | "ACT/360", 362 | symbol_raw=dollar, 363 | html="$", 364 | ) 365 | insert( 366 | "COP", 367 | "170", 368 | "CL", 369 | 190000, 370 | "Colombian Peso", 371 | 2, 372 | "CO", 373 | "ACT/360", 374 | "ACT/360", 375 | symbol_raw=dollar, 376 | html="$", 377 | ) 378 | # TODO: Check towletters code and position 379 | insert( 380 | "JMD", 381 | "388", 382 | "JD", 383 | 410, 384 | "Jamaican Dollar", 385 | dfr, 386 | "JM", 387 | "ACT/360", 388 | "ACT/360", 389 | symbol_raw=dollar, 390 | html="$", 391 | ) 392 | # TODO: Check towletters code and position 393 | insert( 394 | "TTD", 395 | "780", 396 | "TT", 397 | 410, 398 | "Trinidad and Tobago Dollar", 399 | dfr, 400 | "TT", 401 | "ACT/360", 402 | "ACT/360", 403 | symbol_raw=dollar, 404 | html="$", 405 | ) 406 | # TODO: Check towletters code and position 407 | insert( 408 | "BMD", 409 | "060", 410 | "BD", 411 | 410, 412 | "Bermudian Dollar", 413 | dfr, 414 | "BM", 415 | symbol_raw=dollar, 416 | html="$", 417 | ) 418 | 419 | # EASTERN EUROPE 420 | insert( 421 | "CZK", 422 | "203", 423 | "CK", 424 | 28, 425 | "Czech Koruna", 426 | dfr, 427 | "CZ", 428 | "ACT/360", 429 | "ACT/360", 430 | symbol_raw=r"\u004b\u010d", 431 | ) 432 | insert( 433 | "PLN", 434 | "985", 435 | "PZ", 436 | 29, 437 | "Polish Złoty", 438 | dfr, 439 | "PL", 440 | "ACT/ACT", 441 | "ACT/365", 442 | symbol_raw=r"\u007a\u0142", 443 | ) 444 | insert( 445 | "TRY", 446 | "949", 447 | "TY", 448 | 30, 449 | "Turkish Lira", 450 | dfr, 451 | "TR", 452 | "ACT/360", 453 | "ACT/360", 454 | symbol_raw=r"\u0054\u004c", 455 | ) 456 | insert( 457 | "HUF", 458 | "348", 459 | "HF", 460 | 32, 461 | "Hungarian Forint", 462 | dfr, 463 | "HU", 464 | "ACT/365", 465 | "ACT/360", 466 | symbol_raw=r"Ft", 467 | html="Ft", 468 | ) 469 | insert( 470 | "RON", 471 | "946", 472 | "RN", 473 | 34, 474 | "Romanian Leu", 475 | dfr, 476 | "RO", 477 | "ACT/360", 478 | "ACT/360", 479 | ) 480 | insert( 481 | "UAH", 482 | "980", 483 | "UH", 484 | 35, 485 | "Ukrainian Hryvnia", 486 | dfr, 487 | "UA", 488 | "ACT/ACT", 489 | "ACT/ACT", 490 | symbol_raw=r"\u20b4", 491 | html="₴", 492 | ) 493 | insert( 494 | "RUB", 495 | "643", 496 | "RR", 497 | 36, 498 | "Russian Ruble", 499 | dfr, 500 | "RU", 501 | "ACT/ACT", 502 | "ACT/ACT", 503 | symbol_raw=r"\u0440\u0443\u0431", 504 | ) 505 | # TODO: Check towletters code and position 506 | insert( 507 | "KZT", 508 | "398", 509 | "KT", 510 | 410, 511 | "Tenge", 512 | dfr, 513 | "KZ", 514 | symbol_raw=r"\u20b8", 515 | html="₸", 516 | ) 517 | # TODO: Check towletters code and position 518 | insert( 519 | "BGN", 520 | "975", 521 | "BN", 522 | 410, 523 | "Bulgarian Lev", 524 | dfr, 525 | "BG", 526 | symbol_raw=r"\u043b\u0432.", 527 | html="лв", 528 | ) 529 | 530 | # MIDDLE EAST & AFRICA 531 | insert( 532 | "ILS", 533 | "376", 534 | "IS", 535 | 410, 536 | "Israeli Shekel", 537 | dfr, 538 | "IL", 539 | "ACT/365", 540 | "ACT/365", 541 | symbol_raw=r"\u20aa", 542 | html="₪", 543 | ) 544 | # TODO: Check towletters code and position 545 | insert( 546 | "AED", 547 | "784", 548 | "AE", 549 | 410, 550 | "United Arab Emirates Dirham", 551 | dfr, 552 | "AE", 553 | ) 554 | # TODO: Check towletters code and position 555 | insert( 556 | "QAR", 557 | "634", 558 | "QA", 559 | 410, 560 | "Qatari Riyal", 561 | dfr, 562 | "QA", 563 | symbol_raw=r"\ufdfc", 564 | html="﷼", 565 | ) 566 | # TODO: Check towletters code and position 567 | insert( 568 | "SAR", 569 | "682", 570 | "SR", 571 | 410, 572 | "Saudi Riyal", 573 | dfr, 574 | "SA", 575 | symbol_raw=r"\ufdfc", 576 | html="﷼", 577 | ) 578 | insert( 579 | "EGP", 580 | "818", 581 | "EP", 582 | 550, 583 | "Egyptian Pound", 584 | dfr, 585 | "EG", 586 | symbol_raw=r"\u00a3", 587 | html="£", 588 | ) 589 | insert( 590 | "NGN", 591 | "566", 592 | "NG", 593 | 650, 594 | "Nigerian Naira", 595 | dfr, 596 | "NG", 597 | symbol_raw=r"\u20a6", 598 | html="₦", 599 | ) 600 | insert( 601 | "ZAR", 602 | "710", 603 | "SA", 604 | 750, 605 | "South African Rand", 606 | dfr, 607 | "ZA", 608 | "ACT/365", 609 | "ACT/365", 610 | symbol_raw=r"R", 611 | html="R", 612 | ) 613 | 614 | # BITCOIN 615 | insert( 616 | "XBT", 617 | "000", 618 | "BT", 619 | -1, 620 | "Bitcoin", 621 | 8, 622 | "WW", 623 | symbol_raw=r"\u0e3f", 624 | html="฿", 625 | ) 626 | -------------------------------------------------------------------------------- /ccy/core/daycounter.py: -------------------------------------------------------------------------------- 1 | """Day Counter for Counting time between 2 dates. 2 | Implemented:: 3 | 4 | * Actual 360 5 | * Actual 365 6 | * 30 / 360 7 | * Actual Actual 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from copy import copy 13 | from datetime import date 14 | from typing import Any 15 | from ..dates.utils import date_diff 16 | 17 | __all__ = ["getdc", "DayCounter", "alldc"] 18 | 19 | 20 | def getdc(name: str) -> DayCounter: 21 | return _day_counters[name]() 22 | 23 | 24 | def alldc() -> dict[str, DayCounterMeta]: 25 | global _day_counters 26 | return copy(_day_counters) 27 | 28 | 29 | class DayCounterMeta(type): 30 | def __new__(cls, name: str, bases: Any, attrs: Any) -> DayCounterMeta: 31 | new_class = super(DayCounterMeta, cls).__new__(cls, name, bases, attrs) 32 | if name := getattr(new_class, "name", ""): 33 | _day_counters[name] = new_class 34 | return new_class 35 | 36 | 37 | _day_counters: dict[str, DayCounterMeta] = {} 38 | 39 | 40 | class DayCounter(metaclass=DayCounterMeta): 41 | name: str = "" 42 | 43 | def count(self, start: date, end: date) -> float: 44 | """Count the number of days between 2 dates""" 45 | return date_diff(end, start).total_seconds() / 86400 46 | 47 | def dcf(self, start: date, end: date) -> float: 48 | return self.count(start, end) / 360.0 49 | 50 | 51 | class Act360(DayCounter): 52 | name = "ACT/360" 53 | 54 | 55 | class Act365(DayCounter): 56 | name = "ACT/365" 57 | 58 | def dcf(self, start: date, end: date) -> float: 59 | return self.count(start, end) / 365.0 60 | 61 | 62 | class Thirty360(DayCounter): 63 | name = "30/360" 64 | 65 | def dcf(self, start: date, end: date) -> float: 66 | d1 = min(start.day, 30) 67 | d2 = min(end.day, 30) 68 | return 360 * (end.year - start.year) + 30 * (end.month - start.month) + d2 - d1 69 | 70 | 71 | class ActAct(DayCounter): 72 | name = "ACT/ACT" 73 | 74 | def dcf(self, start: date, end: date) -> float: 75 | return self.act_act_years(end) - self.act_act_years(start) 76 | 77 | def act_act_years(self, dt: date) -> float: 78 | y = dt.year 79 | days_in_year = 365 if y % 4 else 366 80 | dd = date_diff(dt, date(y, 1, 1)).total_seconds() / 86400 81 | return y + dd / days_in_year 82 | -------------------------------------------------------------------------------- /ccy/dates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/ccy/a322e3730f9ec1fbda86d9614254013efa05c867/ccy/dates/__init__.py -------------------------------------------------------------------------------- /ccy/dates/converters.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import date, datetime 3 | from typing import Any 4 | 5 | try: 6 | from dateutil.parser import parse as date_from_string 7 | except ImportError: # noqa 8 | 9 | def date_from_string(dte): # type: ignore 10 | raise NotImplementedError 11 | 12 | 13 | def todate(val: Any) -> date: 14 | """Convert val to a datetime.date instance by trying several 15 | conversion algorithm. 16 | If it fails it raise a ValueError exception. 17 | """ 18 | if not val: 19 | raise ValueError("Value not provided") 20 | if isinstance(val, datetime): 21 | return val.date() 22 | elif isinstance(val, date): 23 | return val 24 | else: 25 | try: 26 | ival = int(val) 27 | sval = str(ival) 28 | if len(sval) == 8: 29 | return yyyymmdd2date(val) 30 | elif len(sval) == 5: 31 | return juldate2date(val) 32 | else: 33 | raise ValueError 34 | except Exception: 35 | # Try to convert using the parsing algorithm 36 | try: 37 | return date_from_string(val).date() 38 | except Exception: 39 | raise ValueError("Could not convert %s to date" % val) 40 | 41 | 42 | def date2timestamp(dte: date) -> float: 43 | return time.mktime(dte.timetuple()) 44 | 45 | 46 | def jstimestamp(dte: date) -> float: 47 | """Convert a date to a javascript timestamp. 48 | 49 | A Javascript timestamp is the number of milliseconds since 50 | January 1, 1970 00:00:00 UTC.""" 51 | return 1000 * date2timestamp(dte) 52 | 53 | 54 | def timestamp2date(tstamp: float) -> date: 55 | "Converts a unix timestamp to a Python datetime object" 56 | dt = datetime.fromtimestamp(tstamp) 57 | if not dt.hour + dt.minute + dt.second + dt.microsecond: 58 | return dt.date() 59 | else: 60 | return dt 61 | 62 | 63 | def yyyymmdd2date(dte: float | int) -> date: 64 | try: 65 | y = int(dte // 10000) 66 | md = dte % 10000 67 | m = int(md // 100) 68 | d = int(md % 100) 69 | return date(y, m, d) 70 | except Exception: 71 | raise ValueError("Could not convert %s to date" % dte) 72 | 73 | 74 | def date2yyyymmdd(dte: date) -> int: 75 | return dte.day + 100 * (dte.month + 100 * dte.year) 76 | 77 | 78 | def juldate2date(val: float | int) -> date: 79 | """Convert from a Julian date/datetime to python date or datetime""" 80 | ival = int(val) 81 | dec = val - ival 82 | try: 83 | val4 = 4 * ival 84 | yd = val4 % 1461 85 | st = 1899 86 | if yd >= 4: 87 | st = 1900 88 | yd1 = yd - 241 89 | y = val4 // 1461 + st 90 | if yd1 >= 0: 91 | q = yd1 // 4 * 5 + 308 92 | qq = q // 153 93 | qr = q % 153 94 | else: 95 | q = yd // 4 * 5 + 1833 96 | qq = q // 153 97 | qr = q % 153 98 | m = qq % 12 + 1 99 | d = qr // 5 + 1 100 | except Exception: 101 | raise ValueError("Could not convert %s to date" % val) 102 | if dec: 103 | dec24 = 24 * dec 104 | hours = int(dec24) 105 | minutes = int(60 * (dec24 - hours)) 106 | tot_seconds = 60 * (60 * (dec24 - hours) - minutes) 107 | seconds = int(tot_seconds) 108 | microseconds = int(1000000 * (tot_seconds - seconds)) 109 | return datetime(y, m, d, hours, minutes, seconds, microseconds) 110 | else: 111 | return date(y, m, d) 112 | 113 | 114 | def date2juldate(val: date) -> float: 115 | """Convert from a python date/datetime to a Julian date & time""" 116 | f = 12 * val.year + val.month - 22803 117 | fq = f // 12 118 | fr = f % 12 119 | dt = (fr * 153 + 302) // 5 + val.day + fq * 1461 // 4 120 | if isinstance(val, datetime): 121 | return ( 122 | dt 123 | + ( 124 | val.hour 125 | + (val.minute + (val.second + 0.000001 * val.microsecond) / 60.0) / 60.0 126 | ) 127 | / 24.0 128 | ) 129 | else: 130 | return dt 131 | -------------------------------------------------------------------------------- /ccy/dates/futures.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | future_month_list = ["F", "G", "H", "J", "K", "M", "N", "Q", "U", "V", "X", "Z"] 4 | 5 | short_month: tuple[str, ...] = ( 6 | "Jan", 7 | "Feb", 8 | "Mar", 9 | "Apr", 10 | "May", 11 | "Jun", 12 | "Jul", 13 | "Aug", 14 | "Sep", 15 | "Oct", 16 | "Nov", 17 | "Dec", 18 | ) 19 | 20 | 21 | def future_date_to_code(dte: date) -> str: 22 | """Obtain a future code from a date. 23 | 24 | For example december 2010 will result in Z10. 25 | """ 26 | return "%s%s" % (future_month_list[dte.month - 1], str(dte.year)[2:]) 27 | 28 | 29 | future_month_dict = dict( 30 | (future_month_list[i], (i + 1, short_month[i])) for i in range(0, 12) 31 | ) 32 | -------------------------------------------------------------------------------- /ccy/dates/period.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | 6 | def period(pstr: str = "") -> Period: 7 | """Create a period object from a period string""" 8 | return Period.make(pstr) 9 | 10 | 11 | def find_first_of(st: str, possible: str) -> int: 12 | lowi = -1 13 | for p in tuple(possible): 14 | i = st.find(p) 15 | if i != -1 and (i < lowi or lowi == -1): 16 | lowi = i 17 | return lowi 18 | 19 | 20 | def safediv(x: int, d: int) -> int: 21 | return x // d if x >= 0 else -(-x // d) 22 | 23 | 24 | def safemod(x: int, d: int) -> int: 25 | return x % d if x >= 0 else -(-x % d) 26 | 27 | 28 | class Period: 29 | def __init__(self, months: int = 0, days: int = 0) -> None: 30 | self._months = months 31 | self._days = days 32 | 33 | @classmethod 34 | def make(cls, data: Any) -> Period: 35 | if isinstance(data, cls): 36 | return data 37 | elif isinstance(data, str): 38 | return cls().add_tenure(data) 39 | else: 40 | raise TypeError("Cannot convert %s to Period" % data) 41 | 42 | def isempty(self) -> bool: 43 | return self._months == 0 and self._days == 0 44 | 45 | def add_days(self, days: int) -> None: 46 | self._days += days 47 | 48 | def add_weeks(self, weeks: int) -> None: 49 | self._days += int(7 * weeks) 50 | 51 | def add_months(self, months: int) -> None: 52 | self._months += months 53 | 54 | def add_years(self, years: int) -> None: 55 | self._months += int(12 * years) 56 | 57 | @property 58 | def years(self) -> int: 59 | return safediv(self._months, 12) 60 | 61 | @property 62 | def months(self) -> int: 63 | return safemod(self._months, 12) 64 | 65 | @property 66 | def weeks(self) -> int: 67 | return safediv(self._days, 7) 68 | 69 | @property 70 | def days(self) -> int: 71 | return safemod(self._days, 7) 72 | 73 | @property 74 | def totaldays(self) -> int: 75 | return 30 * self._months + self._days 76 | 77 | def __repr__(self) -> str: 78 | """The period string""" 79 | return self.components() 80 | 81 | def __str__(self) -> str: 82 | return self.__repr__() 83 | 84 | def components(self) -> str: 85 | """The period string""" 86 | p = "" 87 | neg = self.totaldays < 0 88 | y = self.years 89 | m = self.months 90 | w = self.weeks 91 | d = self.days 92 | if y: 93 | p = "%sY" % abs(y) 94 | if m: 95 | p = "%s%sM" % (p, abs(m)) 96 | if w: 97 | p = "%s%sW" % (p, abs(w)) 98 | if d: 99 | p = "%s%sD" % (p, abs(d)) 100 | return "-" + p if neg else p 101 | 102 | def simple(self) -> str: 103 | """A string representation with only one period delimiter.""" 104 | if self._days: 105 | return "%sD" % self.totaldays 106 | elif self.months: 107 | return "%sM" % self._months 108 | elif self.years: 109 | return "%sY" % self.years 110 | else: 111 | return "" 112 | 113 | def add_tenure(self, pstr: str) -> Period: 114 | if isinstance(pstr, self.__class__): 115 | self._months += pstr._months 116 | self._days += pstr._days 117 | return self 118 | st = str(pstr).upper() 119 | done = False 120 | sign = 1 121 | while not done: 122 | if not st: 123 | done = True 124 | else: 125 | ip = find_first_of(st, "DWMY") 126 | if ip == -1: 127 | raise ValueError("Unknown period %s" % pstr) 128 | p = st[ip] 129 | v = int(st[:ip]) 130 | sign = sign if v > 0 else -sign 131 | v = sign * abs(v) 132 | if p == "D": 133 | self.add_days(v) 134 | elif p == "W": 135 | self.add_weeks(v) 136 | elif p == "M": 137 | self.add_months(v) 138 | elif p == "Y": 139 | self.add_years(v) 140 | ip += 1 141 | st = st[ip:] 142 | return self 143 | 144 | def __add__(self, other: Any) -> Period: 145 | p = self.make(other) 146 | return self.__class__(self._months + p._months, self._days + p._days) 147 | 148 | def __radd__(self, other: Any) -> Period: 149 | return self + other 150 | 151 | def __sub__(self, other: Any) -> Period: 152 | p = self.make(other) 153 | return self.__class__(self._months - p._months, self._days - p._days) 154 | 155 | def __rsub__(self, other: Any) -> Period: 156 | return self.make(other) - self 157 | 158 | def __gt__(self, other: Any) -> bool: 159 | return self.totaldays > self.make(other).totaldays 160 | 161 | def __lt__(self, other: Any) -> bool: 162 | return self.totaldays < self.make(other).totaldays 163 | 164 | def __ge__(self, other: Any) -> bool: 165 | return self.totaldays >= self.make(other).totaldays 166 | 167 | def __le__(self, other: Any) -> bool: 168 | return self.totaldays <= self.make(other).totaldays 169 | 170 | def __eq__(self, other: Any) -> bool: 171 | return self.totaldays == self.make(other).totaldays 172 | -------------------------------------------------------------------------------- /ccy/dates/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime, timezone, timedelta 2 | 3 | 4 | def utcnow() -> datetime: 5 | return datetime.now(timezone.utc) 6 | 7 | 8 | def as_utc(dt: date | None = None) -> datetime: 9 | if dt is None: 10 | return utcnow() 11 | elif isinstance(dt, datetime): 12 | return dt.astimezone(timezone.utc) 13 | else: 14 | return datetime(dt.year, dt.month, dt.day, tzinfo=timezone.utc) 15 | 16 | 17 | def date_diff(a: date, b: date) -> timedelta: 18 | if isinstance(a, datetime) or isinstance(b, datetime): 19 | return as_utc(a) - as_utc(b) 20 | return a - b 21 | -------------------------------------------------------------------------------- /ccy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/ccy/a322e3730f9ec1fbda86d9614254013efa05c867/ccy/py.typed -------------------------------------------------------------------------------- /ccy/tradingcentres/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import date, timedelta 5 | import holidays 6 | import holidays.countries 7 | import holidays.financial 8 | 9 | isoweekend = frozenset((6, 7)) 10 | oneday = timedelta(days=1) 11 | 12 | trading_centres: dict[str, TradingCentre] = {} 13 | 14 | 15 | def prevbizday(dte: date, nd: int = 1, tcs: str | None = None) -> date: 16 | return centres(tcs).prevbizday(dte, nd) 17 | 18 | 19 | def nextbizday(dte: date, nd: int = 1, tcs: str | None = None) -> date: 20 | return centres(tcs).nextbizday(dte, nd) 21 | 22 | 23 | def centres(codes: str | None = None) -> TradingCentres: 24 | tcs = TradingCentres() 25 | if codes: 26 | lcs = codes.upper().replace(" ", "").split(",") 27 | for code in lcs: 28 | tc = trading_centres.get(code) 29 | if tc: 30 | tcs.centres[tc.code] = tc 31 | return tcs 32 | 33 | 34 | @dataclass 35 | class TradingCentre: 36 | code: str 37 | calendar: holidays.HolidayBase 38 | 39 | def isholiday(self, dte: date) -> bool: 40 | return dte in self.calendar 41 | 42 | 43 | @dataclass 44 | class TradingCentres: 45 | centres: dict[str, TradingCentre] = field(default_factory=dict) 46 | 47 | @property 48 | def code(self) -> str: 49 | return ",".join(sorted(self.centres)) 50 | 51 | def isbizday(self, dte: date) -> bool: 52 | if dte.isoweekday() in isoweekend: 53 | return False 54 | for c in self.centres.values(): 55 | if c.isholiday(dte): 56 | return False 57 | return True 58 | 59 | def nextbizday(self, dte: date, nd: int = 1) -> date: 60 | n = 0 61 | while not self.isbizday(dte): 62 | dte += oneday 63 | while n < nd: 64 | dte += oneday 65 | if self.isbizday(dte): 66 | n += 1 67 | return dte 68 | 69 | def prevbizday(self, dte: date, nd: int = 1) -> date: 70 | n = 0 71 | if nd < 0: 72 | return self.nextbizday(dte, -nd) 73 | else: 74 | while not self.isbizday(dte): 75 | dte -= oneday 76 | n = 0 77 | while n < nd: 78 | dte -= oneday 79 | if self.isbizday(dte): 80 | n += 1 81 | return dte 82 | 83 | 84 | trading_centres.update( 85 | (tc.code, tc) 86 | for tc in ( 87 | TradingCentre( 88 | code="TGT", 89 | calendar=holidays.financial.european_central_bank.EuropeanCentralBank(), # type: ignore [no-untyped-call] 90 | ), 91 | TradingCentre( 92 | code="LON", 93 | calendar=holidays.countries.united_kingdom.UnitedKingdom(), # type: ignore [no-untyped-call] 94 | ), 95 | TradingCentre( 96 | code="NY", 97 | calendar=holidays.countries.united_states.UnitedStates(), # type: ignore [no-untyped-call] 98 | ), 99 | ) 100 | ) 101 | -------------------------------------------------------------------------------- /dev/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install -U pip poetry 4 | poetry install --all-extras --with book 5 | -------------------------------------------------------------------------------- /dev/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | BLACK_ARG="--check" 5 | RUFF_ARG="" 6 | 7 | if [ "$1" = "fix" ] ; then 8 | BLACK_ARG="" 9 | RUFF_ARG="--fix" 10 | fi 11 | 12 | echo "run black" 13 | black ccy tests --exclude "fluid_common/protos/v2|fluid_apps/db/migrations" ${BLACK_ARG} 14 | echo "run ruff" 15 | ruff check ccy tests ${RUFF_ARG} 16 | echo "run mypy" 17 | mypy ccy 18 | -------------------------------------------------------------------------------- /dev/start-jupyter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | PORT=$1 5 | 6 | export PYTHONPATH=${PWD}:${PYTHONPATH} 7 | ENV_FILE="${PWD}/.env" 8 | touch ${ENV_FILE} 9 | export $(grep -v '^#' ${ENV_FILE} | xargs) 10 | 11 | jupyter-lab --port=${PORT} 12 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Book settings 2 | # Learn more at https://jupyterbook.org/customize/config.html 3 | 4 | title: Python Currencies 5 | author: Quantmind team 6 | 7 | # Force re-execution of notebooks on each build. 8 | # See https://jupyterbook.org/content/execute.html 9 | execute: 10 | execute_notebooks: force 11 | 12 | # Define the name of the latex output file for PDF builds 13 | latex: 14 | latex_documents: 15 | targetname: book.tex 16 | 17 | # Information about where the book exists on the web 18 | repository: 19 | url: https://github.com/quantmind/ccy # Online location of your book 20 | path_to_book: docs # Optional path to your book, relative to the repository root 21 | branch: main # Which branch of the repository should be used when creating links (optional) 22 | 23 | # Add GitHub buttons to your book 24 | # See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository 25 | html: 26 | use_issues_button: true 27 | use_repository_button: true 28 | -------------------------------------------------------------------------------- /docs/_toc.yml: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | # Learn more at https://jupyterbook.org/customize/toc.html 3 | 4 | format: jb-book 5 | root: intro 6 | chapters: 7 | - file: dates 8 | -------------------------------------------------------------------------------- /docs/dates.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | extension: .md 5 | format_name: myst 6 | format_version: 0.13 7 | jupytext_version: 1.14.7 8 | kernelspec: 9 | display_name: Python 3 (ipykernel) 10 | language: python 11 | name: python3 12 | --- 13 | 14 | # Dates & Periods 15 | 16 | The module is shipped with a ``date`` module for manipulating time periods and 17 | converting dates between different formats. Th *period* function can be use 18 | to create ``Period`` instanc.:: 19 | 20 | ```{code-cell} ipython3 21 | import ccy 22 | p = ccy.period("1m") 23 | p 24 | ``` 25 | 26 | ```{code-cell} ipython3 27 | p += "2w" 28 | p 29 | ``` 30 | 31 | ```{code-cell} ipython3 32 | p += "3m" 33 | p 34 | ``` 35 | 36 | ```{code-cell} ipython3 37 | p -= "1w" 38 | p 39 | ``` 40 | 41 | ```{code-cell} ipython3 42 | p -= "1w" 43 | p 44 | ``` 45 | 46 | ```{code-cell} ipython3 47 | p -= "1w" 48 | p 49 | ``` 50 | 51 | ```{code-cell} ipython3 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | extension: .md 5 | format_name: myst 6 | format_version: 0.13 7 | jupytext_version: 1.14.7 8 | kernelspec: 9 | display_name: Python 3 (ipykernel) 10 | language: python 11 | name: python3 12 | --- 13 | 14 | # Python CCY 15 | 16 | ## Getting Started 17 | 18 | * installation 19 | ```bash 20 | pip install ccy 21 | ``` 22 | * display currencies 23 | 24 | ```{code-cell} ipython3 25 | import ccy 26 | import pandas as pd 27 | df = pd.DataFrame(ccy.dump_currency_table()) 28 | df.head(80) 29 | ``` 30 | 31 | ## Main Usage 32 | 33 | ```{code-cell} ipython3 34 | import ccy 35 | eur = ccy.currency("aud") 36 | eur.printinfo() 37 | ``` 38 | 39 | a currency object has the following properties: 40 | * *code*: the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) three letters codee. 41 | * *twoletterscode*: two letter crg. 42 | * *default_country*: the default [ISO 3166-1 alpha_2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code for the currency. 43 | * *isonumber*: the ISO 4217 number. 44 | * *name*: the name of the currency. 45 | * *order*: default ordering in currency pairs (more of this below). 46 | * *rounding*: number of decimal places 47 | 48 | +++ 49 | 50 | ## Currency Crosses 51 | 52 | 53 | You can create currency pairs by using the ``currency_pair`` functn:: 54 | 55 | ```{code-cell} ipython3 56 | c = ccy.currency_pair("eurusd") 57 | c 58 | ``` 59 | 60 | ```{code-cell} ipython3 61 | c.mkt() 62 | ``` 63 | 64 | ```{code-cell} ipython3 65 | c = ccy.currency_pair("chfusd") 66 | c 67 | ``` 68 | 69 | ```{code-cell} ipython3 70 | c.mkt() # market convention pair 71 | ``` 72 | 73 | ## cross & crossover 74 | 75 | Some shortcuts: 76 | 77 | ```{code-cell} ipython3 78 | ccy.cross("aud") 79 | ``` 80 | 81 | ```{code-cell} ipython3 82 | ccy.crossover('eur') 83 | ``` 84 | 85 | ```{code-cell} ipython3 86 | ccy.crossover('chf') 87 | ``` 88 | 89 | ```{code-cell} ipython3 90 | ccy.crossover('aud') 91 | ``` 92 | 93 | Note, the Swiss franc cross is represented as 'USD/CHF', while the Aussie Dollar and Euro crosses are represented with the USD as denominator. This is the market convention which is handled by the order property of a currency object. 94 | 95 | +++ 96 | 97 | ## Eurozone 98 | 99 | The euro area, commonly called the eurozone (EZ), is a currency union of 20 member states of the European Union (EU) that have adopted the euro (€) as their primary currency and sole legal tender, and have thus fully implemented EMU policies. 100 | 101 | ```{code-cell} ipython3 102 | ccy.eurozone 103 | ``` 104 | 105 | ```{code-cell} ipython3 106 | ccy.print_eurozone() 107 | ``` 108 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ccy" 3 | version = "1.7.1" 4 | description = "Python currencies" 5 | authors = ["Luca Sbardella "] 6 | license = "BSD" 7 | readme = "readme.md" 8 | packages = [ 9 | { include = "ccy" } 10 | ] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.10" 14 | python-dateutil = "^2.9.0" 15 | pycountry = "^24.6.1" 16 | rich = {version = "^13.7.1", optional = true} 17 | click = {version = "^8.1.7", optional = true} 18 | pandas = {version = "^2.0.3", optional = true} 19 | holidays = {version = "^0.63", optional = true} 20 | 21 | 22 | [tool.poetry.group.dev.dependencies] 23 | black = "^24.3.0" 24 | isort = "^5.10.1" 25 | pytest-cov = "^6.0.0" 26 | mypy = "^1.14.1" 27 | ruff = "^0.8.4" 28 | types-python-dateutil = "^2.9.0.20241003" 29 | 30 | [tool.poetry.extras] 31 | cli = ["rich", "click", "pandas"] 32 | holidays = ["holidays"] 33 | 34 | [tool.poetry.group.book] 35 | optional = true 36 | 37 | [tool.poetry.group.book.dependencies] 38 | jupyter-book = "^1.0.3" 39 | jupyterlab = "^4.3.4" 40 | jupytext = "^1.16.6" 41 | ghp-import = "^2.1.0" 42 | 43 | 44 | [tool.poetry.scripts] 45 | ccys = "ccy.cli:ccys" 46 | 47 | 48 | [build-system] 49 | requires = ["poetry-core"] 50 | build-backend = "poetry.core.masonry.api" 51 | 52 | [tool.jupytext] 53 | formats = "ipynb,myst" 54 | 55 | [tool.isort] 56 | profile = "black" 57 | 58 | [tool.ruff] 59 | lint.select = ["E", "F"] 60 | line-length = 88 61 | 62 | [tool.mypy] 63 | disallow_untyped_calls = true 64 | disallow_untyped_defs = true 65 | warn_no_return = true 66 | 67 | [[tool.mypy.overrides]] 68 | module = [ 69 | "pycountry.*", 70 | "pandas.*", 71 | ] 72 | ignore_missing_imports = true 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Python CCY 2 | 3 | [![PyPI version](https://badge.fury.io/py/ccy.svg)](https://badge.fury.io/py/ccy) 4 | [![Python versions](https://img.shields.io/pypi/pyversions/ccy.svg)](https://pypi.org/project/ccy) 5 | [![Python downloads](https://img.shields.io/pypi/dd/ccy.svg)](https://pypi.org/project/ccy) 6 | [![build](https://github.com/quantmind/ccy/actions/workflows/build.yml/badge.svg)](https://github.com/quantmind/ccy/actions/workflows/build.yml) 7 | [![codecov](https://codecov.io/gh/quantmind/ccy/branch/main/graph/badge.svg?token=2L5SY0WkXv)](https://codecov.io/gh/quantmind/ccy) 8 | 9 | A python module for currencies. The module compiles a dictionary of 10 | currency objects containing information useful in financial analysis. 11 | Not all currencies in the world are supported yet. You are welcome to 12 | join and add more. 13 | 14 | * Documentation is available as [ccy jupyter book](https://quantmind.github.io/ccy/). 15 | * Install the command line tool with `pip install ccy[cli]`. 16 | * Show all currencies in the command line via `ccys show` 17 | * Trading centres are available when installing the `holidays` extra via 18 | ``` 19 | pip install ccy[holidays] 20 | ``` 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/ccy/a322e3730f9ec1fbda86d9614254013efa05c867/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_ccy.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from io import StringIO as StreamIO 4 | 5 | import ccy as ccym 6 | import pytest 7 | from ccy import ( 8 | CountryError, 9 | ccypair, 10 | countryccy, 11 | currency, 12 | currency_pair, 13 | currencydb, 14 | dump_currency_table, 15 | set_new_country, 16 | ) 17 | from ccy.core.country import eurozone 18 | 19 | 20 | def test_codes(): 21 | ccys = ccym.all() 22 | assert "USD" in ccys 23 | assert len(ccym.g7()) == 4 24 | assert len(ccym.g10()) == 7 25 | assert len(set(ccym.g7())) == 4 26 | assert len(set(ccym.g10())) == 7 27 | 28 | 29 | def test_defaultcountry(): 30 | ccys = currencydb() 31 | for ccy in ccys.values(): 32 | if ccy.code != "XBT": 33 | assert ccy.code[:2] == ccy.default_country 34 | 35 | 36 | def test_iso(): 37 | ccys = currencydb() 38 | iso = {} 39 | for ccy in ccys.values(): 40 | assert ccy.isonumber not in iso 41 | iso[ccy.isonumber] = ccy 42 | 43 | 44 | def test_repr_and_eq(): 45 | ccy = currency("eur") 46 | assert str(ccy) == "EUR" 47 | assert ccy.symbol == "€" 48 | assert ccy == currency("eur") 49 | assert ccy != "whatever" 50 | 51 | 52 | def test_2letters(): 53 | ccys = currencydb() 54 | twol = {} 55 | for ccy in ccys.values(): 56 | assert ccy.twoletterscode not in twol 57 | twol[ccy.twoletterscode] = ccy 58 | 59 | 60 | def test_new_country(): 61 | with pytest.raises(CountryError): 62 | set_new_country("EU", "EUR", "Eurozone") 63 | 64 | 65 | def test_eurozone(): 66 | assert len(eurozone) == 20 67 | for c in eurozone: 68 | assert countryccy(c) == "EUR" 69 | 70 | 71 | def test_countryccy(): 72 | assert "AUD" == countryccy("au") 73 | assert "EUR" == countryccy("eu") 74 | 75 | 76 | def test_ccy_pair(): 77 | p = ccypair("usdchf") 78 | assert str(p) == "USDCHF" 79 | p = p.over() 80 | assert str(p) == "CHFUSD" 81 | p = ccypair("EURUSD") 82 | assert p == p.over() 83 | 84 | 85 | def test_pickle(): 86 | c = currency("eur") 87 | cd = pickle.dumps(c) 88 | c2 = pickle.loads(cd) 89 | assert c == c2 90 | assert c != "EUR" 91 | 92 | 93 | def test_json(): 94 | c = currency("eur") 95 | info = c.info() 96 | json.dumps(info) 97 | 98 | 99 | def test_swap(): 100 | c1 = currency("eur") 101 | c2 = currency("chf") 102 | inv, a1, a2 = c1.swap(c2) 103 | assert not inv 104 | assert c1 == a1 105 | assert c2 == a2 106 | inv, a1, a2 = c2.swap(c1) 107 | assert inv 108 | assert c1 == a1 109 | assert c2 == a2 110 | 111 | 112 | def test_as_cross(): 113 | c1 = currency("eur") 114 | c2 = currency("chf") 115 | assert c1.as_cross() == "EURUSD" 116 | assert c2.as_cross() == "USDCHF" 117 | assert c1.as_cross("/") == "EUR/USD" 118 | assert c2.as_cross("/") == "USD/CHF" 119 | 120 | 121 | def test_print(): 122 | stream = StreamIO() 123 | c2 = currency("chf") 124 | c2.printinfo(stream) 125 | value = stream.getvalue() 126 | assert value 127 | 128 | 129 | def test_dump_currency_table(): 130 | db = currencydb() 131 | table = dump_currency_table() 132 | assert len(table) == len(db) 133 | 134 | 135 | def test_description(): 136 | c = currency("eur") 137 | assert c.description() == "EUR / USD Spot Exchange Rate" 138 | c = currency("chf") 139 | assert c.description() == "USD / CHF Spot Exchange Rate" 140 | c = currency("usd") 141 | assert c.description() == "Dollar" 142 | 143 | 144 | def test_spot_price(): 145 | c1 = currency("eur") 146 | c2 = currency("gbp") 147 | assert c1.spot(c2, 1.3, 1.6) == 1.3 / 1.6 148 | assert c2.spot(c1, 1.6, 1.3) == 1.3 / 1.6 149 | 150 | 151 | def test_currency_pair(): 152 | p = currency_pair("eurgbp") 153 | assert p.ccy1.code == "EUR" 154 | assert p.ccy2.code == "GBP" 155 | assert p.mkt() == p 156 | p = currency_pair("gbpeur") 157 | assert p.mkt() != p 158 | -------------------------------------------------------------------------------- /tests/test_dates.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | import pytest 4 | from ccy import date2juldate, date2yyyymmdd, juldate2date, period, todate, yyyymmdd2date 5 | 6 | 7 | @pytest.fixture() 8 | def dates(): 9 | return [ 10 | (date(2010, 6, 11), 40340, 20100611, 1276210800), 11 | (date(2009, 4, 2), 39905, 20090402, 1238626800), 12 | (date(1996, 2, 29), 35124, 19960229, 825552000), 13 | (date(1970, 1, 1), 25569, 19700101, 0), 14 | (date(1900, 1, 1), 1, 19000101, None), 15 | ] 16 | 17 | 18 | def test_period(): 19 | a = period("5Y") 20 | assert a.years == 5 21 | b = period("1y3m") 22 | assert b.years == 1 23 | assert b.months == 3 24 | c = period("-3m") 25 | assert c.years == 0 26 | assert c.months == -3 27 | 28 | 29 | def test_add_period(): 30 | a = period("4Y") 31 | b = period("1Y3M") 32 | c = a + b 33 | assert c.years == 5 34 | assert c.months == 3 35 | 36 | 37 | def test_add_string(): 38 | a = period("4y") 39 | assert a + "3m" == period("4y3m") 40 | assert "3m" + a == period("4y3m") 41 | 42 | 43 | def test_subtract_period(): 44 | a = period("4Y") 45 | b = period("1Y") 46 | c = a - b 47 | assert c.years == 3 48 | assert c.months == 0 49 | c = period("3Y") - period("1Y3M") 50 | assert c.years == 1 51 | assert c.months == 9 52 | assert str(c) == "1Y9M" 53 | 54 | 55 | def test_subtract_string(): 56 | a = period("4y") 57 | assert a - "3m" == period("3y9m") 58 | assert "5y" - a == period("1y") 59 | assert "3m" - a == period("-3y9m") 60 | 61 | 62 | def test_compare(): 63 | a = period("4Y") 64 | b = period("4Y") 65 | c = period("1Y2M") 66 | assert a == b 67 | assert a >= b 68 | assert a <= b 69 | assert c <= a 70 | assert c < a 71 | assert (c == a) is False 72 | assert (c >= b) is False 73 | assert c > a - b 74 | 75 | 76 | def test_week(): 77 | p = period("7d") 78 | assert p.weeks == 1 79 | assert str(p) == "1W" 80 | p.add_weeks(3) 81 | assert p.weeks == 4 82 | assert str(p) == "4W" 83 | assert not p.isempty() 84 | p = period("3w2d") 85 | assert not p.isempty() 86 | assert p.weeks == 3 87 | assert str(p) == "3W2D" 88 | 89 | 90 | def test_empty(): 91 | assert not period("3y").isempty() 92 | assert not period("1m").isempty() 93 | assert not period("3d").isempty() 94 | assert period().isempty() 95 | 96 | 97 | def test_addperiod(): 98 | p = period("3m") 99 | a = period("6m") 100 | assert a.add_tenure(p) == a 101 | assert str(a) == "9M" 102 | 103 | 104 | def test_error(): 105 | with pytest.raises(ValueError): 106 | period("5y6g") 107 | 108 | 109 | def test_simple(): 110 | assert period("3m2y").simple() == "27M" 111 | assert period("-3m2y").simple() == "-27M" 112 | assert period("3d2m").simple() == "63D" 113 | assert period("2y").simple() == "2Y" 114 | 115 | 116 | def test_date2JulDate(dates): 117 | for d, jd, y, ts in dates: 118 | assert jd == date2juldate(d) 119 | 120 | 121 | def test_JulDate2Date(dates): 122 | for d, jd, y, ts in dates: 123 | assert d == juldate2date(jd) 124 | 125 | 126 | def test_Date2YyyyMmDd(dates): 127 | for d, jd, y, ts in dates: 128 | assert y == date2yyyymmdd(d) 129 | 130 | 131 | def test_YyyyMmDd2Date(dates): 132 | for d, jd, y, ts in dates: 133 | assert d == yyyymmdd2date(y) 134 | 135 | 136 | def test_datetime2Juldate(): 137 | jd = date2juldate(datetime(2013, 3, 8, 11, 20, 45)) 138 | assert jd == 41341.47274305556 139 | 140 | 141 | def test_Juldate2datetime(): 142 | dt = juldate2date(41341.47274305556) 143 | dt2 = datetime(2013, 3, 8, 11, 20, 45) 144 | assert dt == dt2 145 | 146 | 147 | def test_string(): 148 | target = date(2014, 1, 5) 149 | assert todate("2014 Jan 05") == target 150 | 151 | 152 | def test_todate_nothing(): 153 | with pytest.raises(ValueError): 154 | todate("") 155 | with pytest.raises(ValueError): 156 | todate(None) 157 | 158 | 159 | def test_todate_datetime(): 160 | assert todate(datetime.now()) == date.today() 161 | assert todate(date.today()) == date.today() 162 | 163 | 164 | # def testDate2Timestamp(): 165 | # for d,jd,y,ts in .dates: 166 | # if ts is not None: 167 | # .assertEqual(ts,date2timestamp(d)) 168 | 169 | # def testTimestamp2Date(): 170 | # for d,jd,y,ts in .dates: 171 | # if ts is not None: 172 | # .assertEqual(d,timestamp2date(ts)) 173 | -------------------------------------------------------------------------------- /tests/test_dc.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | import pytest 4 | 5 | from ccy.dates.utils import utcnow 6 | import ccy 7 | 8 | 9 | def test_alldc(): 10 | assert len(ccy.alldc()) == 4 11 | 12 | 13 | def test_getdb(): 14 | for name in ("ACT/365", "ACT/ACT", "ACT/360", "30/360"): 15 | assert ccy.getdc(name).name == name 16 | start = date.today() 17 | assert ccy.getdc(name).count(start, start + timedelta(days=1)) == 1 18 | assert ccy.getdc(name).dcf(start, start + timedelta(days=1)) > 0 19 | 20 | with pytest.raises(KeyError): 21 | ccy.getdc("kaputt") 22 | 23 | 24 | def test_with_datetime(): 25 | for name in ("ACT/365", "ACT/ACT", "ACT/360"): 26 | dc = ccy.getdc(name) 27 | start = utcnow() 28 | dc1 = dc.dcf(start, start.date() + timedelta(days=1)) 29 | dc2 = dc.dcf(start, start + timedelta(days=1)) 30 | assert dc1 > 0 31 | assert dc2 > 0 32 | assert dc1 < dc2, f"{name}" 33 | -------------------------------------------------------------------------------- /tests/test_tcs.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import pytest 4 | from ccy.tradingcentres import nextbizday, prevbizday, centres 5 | 6 | 7 | @pytest.fixture() 8 | def dates(): 9 | return [ 10 | date(2010, 4, 1), # Thu 11 | date(2010, 4, 2), # Fri 12 | date(2010, 4, 3), # Sat 13 | date(2010, 4, 5), # Mon 14 | date(2010, 4, 6), # Tue 15 | ] 16 | 17 | 18 | def test_NextBizDay(dates): 19 | assert dates[1] == nextbizday(dates[0]) 20 | assert dates[4] == nextbizday(dates[1], 2) 21 | 22 | 23 | def test_nextBizDay0(dates): 24 | assert dates[0] == nextbizday(dates[0], 0) 25 | assert dates[3] == nextbizday(dates[2], 0) 26 | 27 | 28 | def test_prevBizDay(dates): 29 | assert dates[0] == prevbizday(dates[1]) 30 | assert dates[1] == prevbizday(dates[3]) 31 | assert dates[1] == prevbizday(dates[4], 2) 32 | assert dates[0] == prevbizday(dates[4], 3) 33 | 34 | 35 | def test_TGT(): 36 | tcs = centres("TGT") 37 | assert tcs.code == "TGT" 38 | assert not tcs.isbizday(date(2009, 12, 25)) 39 | assert not tcs.isbizday(date(2010, 1, 1)) 40 | --------------------------------------------------------------------------------