Skip to content

rmsrosa/UnitfulAssets.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UnitfulAssets.jl

Main Tests Workflow Status codecov License: MIT GitHub repo size Lifecycle Experimental

Logo

A supplemental units package for Unitful.jl, adding units for all currently active currencies, some stocks and commodities, along with tools to perform conversions based on exchange market rates.

This package is currently under development and is not yet registered.

Table of Contents

Summary

Several assets such as cash, stock, and commodity are created as Unitful objects. A new dimension is created for each asset, along with its reference unit. Being an extension of Unitful.jl, asset units play nicely along with Unitful's quantities, units, and dimensions.

An ExchangeMarket type is defined as Dict{AssetsPair,Rate}, in which AssetsPair is a tuple of Strings corresponding to the base and quote assets, and Rate contains a positive Unitful.Quantity with the corresponding quote-ask rate for the pair.

Based on a given ExchangeMarket instance, a conversion can be made from the "quote" asset to the "base" asset. This conversion is implemented as an extended dispatch for Unitful.uconvert.

All defined assets are listed in src/pkgdefaults.jl. Some currency symbols are also defined and are listed in src/currencysymbols.jl.

Installation

This package is compatible with Julia ≥ 1.2 and Unitful ≥ 1.0.

Since it has not been registered yet, it can be installed directly from the github repo in the Julia REPL, by typing ] and adding the url for the repo:

pkg> add https://github.com/rmsrosa/UnitfulAssets.jl

Examples

Let us see some examples using UnitfulAssets.jl.

Cost of raw material for a T-shirt

As an example, consider a T-shirt with a Julia logo that requires as raw material 1.6 square-meters of 150GSM (grams-per-square-meter) cotton fabric at USD$15 per 44 in x 8 yards bolt; two ounces in dyes at USD$20 per pound; one ounce of dye fixer at US$8 per five pounds; and 48 yards in stitching thread at USD$19 per 1000 yards. Then, we may calculate the cost of the raw material as follows.

julia> using Unitful, UnitfulAssets

julia> fabric = 15u"USD"/8u"yd"/44u"inch"
0.04261363636363636 USD inch⁻¹ yd⁻¹

julia> dyes = 20u"USD/lb"
20 USD lb⁻¹

julia> fixer = 8u"USD"/5u"lb"
1.6 USD lb⁻¹

julia> thread = 19u"USD"/1000u"yd"
0.019 USD yd⁻¹

julia> cost_per_t_shirt = 1.6u"m^2" * fabric + 2u"oz" * dyes + 1u"oz" * fixer + 48u"yd" * thread;

julia> println("\nThe cost of raw material per t-shirt is of $cost_per_t_shirt")

The cost of raw material per t-shirt is of 6.447611931829924 USD

Thus, the cost of the raw material is about USD$ 6.45 per T-shirt.

Production cost

Suppose, now, that we have a small business to manufacture the T-shirts above. Besides the raw material expenses, we need eletricity for the sewing machine and the workplace, workers, rent, insurance, and so on. With that in mind, we assume we have a fixed overhead cost of USD$ 24000 per year for rent and the essential utilities, insurance and things like that; eletricity expenses for the sewing machine at USD$ 0.13 per kilowatt-hour; and labor at USD$ 10.50 per worker per hour.

In order to implement that, we add two nondimensional units, namely tshirt and worker, then we define the price constants above and two functions that give us the total cost and total material used. We do this as follows.

julia> using Unitful, UnitfulAssets

julia> module ProductionUnits
           using Unitful
           using Unitful: @unit
           @unit tshirt "tshirt" TShirt 1 false
           @unit worker "worker" Worker 1 false
       end

julia> Unitful.register(ProductionUnits);

julia> fabric = 15u"USD"/8u"yd"/44u"inch"
0.04261363636363636 USD inch⁻¹ yd⁻¹

julia> dyes = 20u"USD/lb"
20 USD lb⁻¹

julia> fixer = 8u"USD"/5u"lb"
1.6 USD lb⁻¹

julia> thread = 19u"USD"/1000u"yd"
0.019 USD yd⁻¹

julia> """
           raw_material(n::Unitful.Quantity)

       Return the amount of each raw material needed to manufacture `n` T-shirts.

       The argument `n` must be given in `tshirt` units.

       Returns a tuple with the following quantities, respectively:
       * The necessary amount of cotton fabric.
       * The necessary amount of dye.
       * The necessary amount of fixer.
       * The necessary amount of thread.

       """
       raw_material(n::Unitful.Quantity) = (1.6u"m^2" * n / u"tshirt", 2u"oz" * n / u"tshirt", 1u"oz" * n / u"tshirt", 48u"yd" * n / u"tshirt")
raw_material

julia> eletricity_price = 0.13u"USD/kW/hr"
0.13 USD hr⁻¹ kW⁻¹

julia> labor_price = 10.50u"USD/worker/hr"
10.5 USD hr⁻¹ worker⁻¹

julia> fixed_cost = 24000u"USD/yr"
24000 USD yr⁻¹

julia> """
           manufacturing_cost(n::Unitful.Quantity, t::Unitful.Quantity, tlim::Unitful.Quantity=40u"hr/worker/wk")

       Return the cost of manufacturing `n` T-shirts during a time period `t`.

       The argument `n` must be given in `tshirt` units, and `t`, in time units.
       The optional argument `tlim` is the time limit of work per worker, which       defaults to `40u"hr/worker/wk"`.

       Return a tuple with the following quantities, respectively:
       * The cost of the production, in US Dollars.
       * The cost per T-shirt.
       * The number of labor hours required to produce `n` t-shirts.
       * The minimum number of workers considering the limit given by `tlim`.
       * The eletricity required for the whole manufacturing process.
       """
       function production_cost(n::Unitful.Quantity, t::Unitful.Quantity, tlim::Unitful.Quantity=40u"hr/worker/wk")
           labor_hours = 2u"hr/tshirt" * n
           eletricity_spent = 2u"kW * hr/tshirt" * n
           total_cost = n * raw_material_price + labor_hours * labor_price + eletricity_spent * eletricity_price + fixed_cost * t
           cost_per_tshirt = total_cost / n
           min_num_workers = Int(ceil(labor_hours/tlim/t)) * u"worker"
           return total_cost, cost_per_tshirt, labor_hours, min_num_workers, eletricity_spent
       end
production_cost

Now, if we want to see the cost and everything else needed to produce 50 T-shirts per week, we do

julia> production_cost(50u"tshirt", 1u"wk")
(1845.3395288296892 USD, 36.906790576593785 USD tshirt⁻¹, 100 hr, 3 worker, 100 hr kW)

julia> raw_material(50u"tshirt")
(80.0 m², 100 oz, 50 oz, 2400 yd)

So, it costs about USD$ 36.91 per T-shirt in this case.

If we want to reduce the cost per T-shirt, we increase production, aiming for 2000 T-shirts per month, with workers working 44 hours per week:

julia> production_cost(2000u"tshirt", 30u"d", 44u"hr/worker/wk")
(57386.47643039496 USD, 28.693238215197482 USD tshirt⁻¹, 4000 hr, 22 worker, 4000 hr kW)

julia> raw_material(2000u"tshirt")
(3200.0 m², 4000 oz, 2000 oz, 96000 yd)

With that, we are able to reduce the cost per T-shirt to about USD$ 28.69.

Exercises:

  1. Add benefit costs for each worker, so that the number of workers properly affects the cost.

  2. Add a linear revenue function proportional to the number of T-shirts sold, with proportionality constant being the selling price per T-shirt.

  3. Add an affine profit function, which is the difference between the revenue function and the cost function.

  4. Find the break-even point, which is the number of T-shirts where profit vanishes, i.e. neiher profit nor loss is incurred.

Continuously varying interest rate

Here we use the package DifferentialEquations.jl.

Suppose we have a £1,000 in a savings account in a British bank, with an expected variable interest rate for the next ten years given by

formula,

and suppose we want to estimate how much we will have after ten years. This can be implemented as follows.

julia> using Unitful, UnitfulAssets, DifferentialEquations

julia> rate(t) = (1.5 + 5(t * u"1/yr")^2 * ( 1 + (t * u"1/yr")^3)^-1)*u"percent/yr"
rate (generic function with 1 method)

julia> f(u,rate,t) = rate(t) * u
f (generic function with 1 method)

julia> tspan = Tuple([0.0,10.0]*u"yr")
(0.0 yr, 10.0 yr)

julia> u₀ = 1000.0u"GBP"
1000.0 GBP

julia> prob = ODEProblem(f,u₀,tspan,rate)
ODEProblem with uType Quantity{Float64,GBPCURRENCY,Unitful.FreeUnits{(GBP,),GBPCURRENCY,nothing}} and tType Quantity{Float64,𝐓,Unitful.FreeUnits{(yr,),𝐓,nothing}}. In-place: false
timespan: (0.0 yr, 10.0 yr)
u0: 1000.0 GBP

julia> savings = solve(prob);

julia> println("After $(savings.t[end]), we expect to have $(savings.u[end])")
After 10.0 yr, we expect to have 1303.6211777402004 GBP

Thus, we expect to have about £1,303.62 in our savings account, after ten years.

Exchange markets

For exchanging/trading assets, we provide a few dispatches of a function generate_exchmkt to generate a ExchangeMarket instance from a single Tuple, an Array or a Dict with AssetsPair and Rate instances. Consider, for example, the following exchange market:

julia> using Unitful, UnitfulAssets

julia> exch_mkt_27nov2020 = generate_exchmkt([
                  ("EUR","USD") => 1.19536, ("USD","EUR") => 0.836570,
                  ("EUR","GBP") => 1.11268, ("GBP","EUR") => 0.898734,
                  ("USD","CAD") => 1.29849, ("CAD","USD") => 0.770125,
                  ("USD","BRL") => 5.33897, ("BRL","USD") => 0.187302
              ])
Dict{UnitfulAssets.AssetsPair,UnitfulAssets.Rate} with 8 entries:
  AssetsPair("USD", "BRL") => Rate(5.33897 BRL USD⁻¹)
  AssetsPair("USD", "EUR") => Rate(0.83657 EUR USD⁻¹)
  AssetsPair("EUR", "GBP") => Rate(1.11268 GBP EUR⁻¹)
  AssetsPair("GBP", "EUR") => Rate(0.898734 EUR GBP⁻¹)
  AssetsPair("USD", "CAD") => Rate(1.29849 CAD USD⁻¹)
  AssetsPair("EUR", "USD") => Rate(1.19536 USD EUR⁻¹)
  AssetsPair("CAD", "USD") => Rate(0.770125 USD CAD⁻¹)
  AssetsPair("BRL", "USD") => Rate(0.187302 USD BRL⁻¹)

Then, the conversions between these currencies can be done as follows:

julia> uconvert(u"BRL", 100u"USD", exch_mkt_27nov2020)
533.8969999999999 BRL

This means that I need about 533.90 BRL to buy 100 USD.

If I have dollars and I want to buy about 500 BRL, we do it the other way around:

julia> uconvert(u"USD", 500u"BRL", exch_mkt_27nov2020)
93.651 USD

Now, if, instead, I have 500 BRL and I want to see how many dollars I can buy with it, I need the same exchange rate as in the first conversion, but in a inverse relation, which is accomplished with the option argument mode=-1, so that

julia> uconvert(u"USD", 500u"BRL", exch_mkt_27nov2020, mode=-1)
93.65102257551551 USD

Another situation is when we don't have a currency pair in the given exchange market, such as ("EUR", "CAD"), which is not in exch_mkt_27nov2020. In this case we can use an intermediate currency, if available. In the example market, USD works. The exchange with an intermediate currency is achieved with mode=2:

julia> uconvert(u"CAD", 100u"EUR", exch_mkt_27nov2020, mode=2)
155.21630064 CAD

Now, if we have 150 CAD and want to see how many Euros we can buy with it, we use mode=-2:

julia> uconvert(u"EUR", 150u"CAD", exch_mkt_27nov2020, mode=-2)
96.63933451674102 EUR

There are also a few dispatches of generate_exchmkt to create ExchangeMarket instances from JSON files downloaed from fixer.io and currencylayer.com forex conversion sites. Further conversion providers should be added in the future. In any case, one can easily add a dispatch for the API of one's choice.

Continuously varying interest rate in a foreign bank

Now, considering again the example above of continuously varying interest rate, suppose that I am actually in Brazil and I want to see the evolution of my savings in terms of Brazillian Reais. Suppose, also, that this happened ten years ago, so we can use some real exchange rates. In this case, I use an exchange rate time series, as follows.

julia> BRLGBP_timeseries = Dict(
           "2011-01-01" => generate_exchmkt(("BRL","GBP") => 0.38585),
           "2012-01-01" => generate_exchmkt(("BRL","GBP") => 0.34587),
           "2013-01-01" => generate_exchmkt(("BRL","GBP") => 0.29998),
           "2014-01-01" => generate_exchmkt(("BRL","GBP") => 0.25562),
           "2015-01-02" => generate_exchmkt(("BRL","GBP") => 0.24153),
           "2016-01-03" => generate_exchmkt(("BRL","GBP") => 0.17093),
           "2017-01-02" => generate_exchmkt(("BRL","GBP") => 0.24888),
           "2018-01-02" => generate_exchmkt(("BRL","GBP") => 0.22569),
           "2019-01-04" => generate_exchmkt(("BRL","GBP") => 0.21082),
           "2020-01-04" => generate_exchmkt(("BRL","GBP") => 0.18784)
       );

julia> uconvert.(u"BRL", 1000u"GBP", values(BRLGBP_timeseries), mode=-1)'
1×10 LinearAlgebra.Adjoint{Quantity{Float64,BRLCURRENCY,Unitful.FreeUnits{(BRL,),BRLCURRENCY,nothing}},Array{Quantity{Float64,BRLCURRENCY,Unitful.FreeUnits{(BRL,),BRLCURRENCY,nothing}},1}}:
 2591.68 BRL  2891.26 BRL  4018.0 BRL  4743.38 BRL    4140.27 BRL  3912.06 BRL  4430.86 BRL  5323.68 BRL

Notice the optional argument mode=-1, so it uses the inverse rate for the conversion. As explained above, this is different than using the rate for the pair ("GBP", "BRL") since we don't want to buy GBP with BRL, and neither do we want the direct rate for ("BRL", "GBP") since we don't want to buy a specific amount of BRL with GBP. Instead, we want to find out how much BRL we can buy with a given amount of GBP, so we use the inverse of the rate ("BRL", "GBP").

Exercise: In the Production cost problem, suppose the raw materials come from a foreign country (or countries). Add an exchange market for properly taking into account the dependency of the production cost, the profit, and the break even point on the foreing currency(ies).

Decimal, fixed decimals and rational rates

Since the type Rate has been defined with of value of type Number, it is possible to work with any subtype of Number, such as Decimal, FixedDecimals and Rational rates. For example, the following code generates an ExchangeMarket instance with Rational rates:

julia> exch_mkt_from_dict_and_rationals = generate_exchmkt(Dict([
                  ("EUR","USD") => 119536//100000, ("USD","EUR") => 836570//1000000
              ]))
Dict{UnitfulAssets.AssetsPair,UnitfulAssets.Rate} with 2 entries:
  AssetsPair("USD", "EUR") => Rate(83657//100000 EUR USD⁻¹)
  AssetsPair("EUR", "USD") => Rate(7471//6250 USD EUR⁻¹)

For Decimal rates, it is similar:

julia> using Decimals

julia> exch_mkt_from_dict_and_decimals = generate_exchmkt(Dict([
           ("EUR","USD") => Decimal(1.19536), ("USD","EUR") => Decimal(0.836570)
       ]))
Dict{UnitfulAssets.AssetsPair,UnitfulAssets.Rate} with 2 entries:
  AssetsPair("USD", "EUR") => Rate(0.83657 EUR USD⁻¹)
  AssetsPair("EUR", "USD") => Rate(1.19536 USD EUR⁻¹)

Similarly for FixedDecimal rates:

julia> using FixedPointDecimals

julia> exch_mkt_from_dict_and_fixeddecimals = generate_exchmkt(Dict([
                         ("EUR","USD") => FixedDecimal{Int,4}(1.19536), ("USD","EUR") => FixedDecimal{Int,4}(0.836570)
                     ]))
Dict{UnitfulAssets.AssetsPair, UnitfulAssets.Rate} with 2 entries:
  AssetsPair("EUR", "USD") => Rate(1.1954 USD EUR⁻¹)
  AssetsPair("USD", "EUR") => Rate(0.8366 EUR USD⁻¹)

Assets rate as Unitful quantity

At some point, I changed the rate from a plain number, such as Rate(1.19536), to a Unitful.Quantity, such as Rate(1.19536 USD EUR⁻¹). With that, the associated unit does not need to be formed each time during the conversion from one assets to the other. It becomes simply a multiplication in Unitful. This is especially useful when broadcasting, significantly speeding up the conversion of arrays of currency quantities.

For instance, the example in Continuously varying interest rate in a foreign bank got more than a 100-fold reduction in speed. These were the respective results in the same machine, using BenchmarkTools, first with the plain rate:

julia> @btime uconvert.(u"BRL", 1000u"GBP", values(BRLGBP_timeseries), mode=-1)'
  2.695 ms (1262 allocations: 71.54 KiB)

and the second with the UnitfulQuantity rate:

julia> @btime uconvert.(u"BRL", 1000u"GBP", values(BRLGBP_timeseries), mode=-1)'
  21.419 μs (282 allocations: 14.82 KiB)

The list of assets

The predefined list of assets is obtained from SNV - Standards Connect the World, more specifically from the xls file in Current currency & funds code list, which is converted to a csv file and then to a julia script that calls the macro to generate the assets.

The list is supposed to contain all currencies currently active in the world. It also contains some bonds and commodity metals, such as gold and platinum. The full list of currencies, bonds and metals defined in this package are given in src/pkgdefaults.jl.

More assets can be added by the user.

Currency symbols

Some currency symbols are also defined as Unitful units, namely the US Dollar symbol US\$, equivalent to USD; the Canadian dollar CA\$,equivalent to CAD; the sterling £, equivalent to GBR; the euro , equivalent to EUR; and the Brazilian Real R$, equivalent to BRL.

Both the euro and the sterling pound symbols are used as units, so that one may use directly 10u"€" and 1u"£". Both are unicode characters that can be obtained in the REPL or in some proper Julia environment by tab completion \euro+[TAB] and \sterling+[TAB].

The dollar sign, however, is a reserved sign in Julia, so we do not use it as a unit symbol, but we do use it as an abbreviation. The unit definitions for US\$, CA\$, and R$ are, respectively, USdollar, CAdollar, and Real, so for instance we have

julia> 1u"USdollar"
1 US$

The list of units with their symbols defined in this package is given in src/currencysymbols.jl

Many symbols are used for different currencies, so we do not attempt to define all symbols here. For instance, the yen sign ¥ is used both for China's Renminbi, with code CNY, also known as Yuan, as well as for Japan's Yen, with code JPY.

If one desires to include the Yen sign for, say China, then one should create a module and not forget about registering the module with Unitful:

module NewUnits
    using Unitful
    using UnitfulAssets

    @unit ¥ "¥" YuanSign 1.0u"CNY" true
end

Unitful.register(NewUnits)

In this case,

julia> 1u"¥"
1 ¥

One can even have the same sign for the Chinese yuan and the Japanese yen as abbreviation but with different symbols:

module NewUnits
    using Unitful
    using UnitfulAssets

    @unit yuan "¥" YuanSign 1.0u"CNY" true
    @unit yen "¥" YenSign 1.0u"JPY" true
end

Unitful.register(NewUnits)

In this case, we have

julia> 1u"yen"
1 ¥

julia> 1u"yuan"
1 ¥

To do

I have been doing this mostly for learning purposes, but also hoping that it will turn out to be a useful package for the community.

There are still a number of things to be added:

  1. See whether it is possible to display currencies as, say USD$ 10.50, instead of 10.50 USD.

  2. See whether it is possible to display 10-fold multiples of a currency in a better way than say kEUR, MEUR, GMEUR, and so on. It would be great to have USD$ 10k, USD$ 10M, and USD$ 10B.

  3. Add tools to read exchange markets from web sources other than fixer.io and currencylayer.com.

  4. Add an option to directly obtain the exchange/trade rates from the web sources using a given API.

  5. Move README examples to a proper Documenter-generated site.

Related packages

After I started writing this package, I found out about bhgomes/UnitfulCurrency.jl, which, however, has been archived for unknown reasons.

Based on bhgomes/UnitfulCurrency, I modified my initial approach of currency pairs to be Rate("EUR", "USD"), instead of a six-length string "EURUSD", for instance.

bhgomes/UnifulCurrency, however, has a single dimension for all currencies, which has the side-effect of allowing to uconvert different quantities without an exchange market rate, on a one-to-one basis. Moreover, all currencies are reference units for the same dimension, which might have further side-effects, although I am not sure.

There is no documentation in bhgomes/UnitfulCurrency, and the README is short. It seems, though, that the exchange markets in bhgomes/UnitfulCurrency are defined for each pair, which is different than our approach, in which an exchange market contains a dictionary of currency pairs, allowing for more flexibility, in my point of view.

Later I also found out about the github organization JuliaFinance, which has the packages JuliaFinance/Assets.jl and JuliaFinance/Currencies.jl, among others. There are some nice concepts there, distinguishing currencies from assets and cash. Take this excerpt for instance:

"When a currency is thought of as a financial instrument (as opposed to a mere label), we choose to refer to it as "Cash" as it would appear, for example, in a balance sheet. Assets.jl provides a Cash instrument together with a specialized Position type that allows for basic algebraic manipulations of Cash and other financial instrument positions".

However, JuliaFinance/Assets.jl is not based on Unitful, so none of the examples above can be easily implemented.

Inspired now by JuliaFinance, and upon realizing that what I have implemented are actually assets, not just currencies, I decided to rename my package to UnitfulAssets.jl. Originally, it was named UnitfulCurrencies.jl.

License

This package is licensed under the MIT license (see file LICENSE in the root directory of the project).