diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index 4a933f4..0000000 --- a/Manifest.toml +++ /dev/null @@ -1,75 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[Countries]] -deps = ["Currencies", "DelimitedFiles"] -path = "../Countries" -uuid = "82bc9f9d-56a2-5453-b677-633a29649040" -version = "0.1.0" - -[[Currencies]] -deps = ["DelimitedFiles"] -path = "../Currencies" -uuid = "0fd90b74-7c1f-579e-9252-02cd883047b9" -version = "0.12.0" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[DelimitedFiles]] -deps = ["Mmap"] -uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" - -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[FinancialInstruments]] -deps = ["Countries", "Dates"] -path = "../FinancialInstruments" -uuid = "fc5b99d0-3725-11e9-2623-bf561589b708" -version = "0.1.0" - -[[FixedPointDecimals]] -deps = ["Printf", "Test"] -git-tree-sha1 = "b17a731934600b835150b7882238e7f06f8be368" -uuid = "fb4d412d-6eee-574d-9565-ede6634db7b0" -version = "0.3.0" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/Project.toml b/Project.toml index 75efba5..a39ef0e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,17 @@ name = "Positions" +desc = "Financial Positions" uuid = "81d999f0-3725-11e9-2196-fd457307c398" -authors = ["Eric Forgy "] -version = "0.1.0" +authors = ["Eric Forgy ", "ScottPJones "] +license = "MIT" +version = "0.2.0" [deps] -FinancialInstruments = "fc5b99d0-3725-11e9-2623-bf561589b708" FixedPointDecimals = "fb4d412d-6eee-574d-9565-ede6634db7b0" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" + +[targets] +test = ["Test"] +build = ["JSON3"] diff --git a/README.md b/README.md index 3adcb40..4e066d1 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ # Positions.jl + +[![Build Status](https://travis-ci.org/JuliaFinance/Positions.jl.svg?branch=master)](https://travis-ci.org/JuliaFinance/Positions.jl) +[![Build status](https://ci.appveyor.com/api/projects/status/chnj7xc6r0deux92/branch/master?svg=true)](https://ci.appveyor.com/project/EricForgy/currencies-jl/branch/master) + +This is a base package for the JuliaFinance ecosytem. + +It provides a singleton type `Currency` based on standard ISO 4167 currency codes to be used primarily for dispatch in other JuliaFinance packages together with three methods: + +- `name`: The name of the currency. +- `code`: The ISO 4167 code for the currency. +- `unit`: The minor unit, i.e. number of decimal places, for the currency. + +(this functionality used to be from the Currencies.jl package) + +These are simple labels, such as `Currency{:USD}`, `Currency{:EUR}`. + +In addition, this package provides an abstract type `FinancialInstrument`. + +A financial instrument is a tradeable monetary contract that creates an asset for some parties while, at same time, creating a liability for others. + +Examples of financial instruments include stocks, bonds, loans, derivatives, etc. However, the most basic financial instruments are currencies. + +When a currency is thought of as a financial instrument (as opposed to a mere label used in UI component), we choose to refer to it as `Cash` (as it would appear in a balance sheet). +The `Cash` type keeps track of the number of minor units as part of the type, for performance and dispatching reasons. + +Short constants are set up, matching the ISO 4167 names, so that you can use +`USD` instead of `Cash{Currency{:USD},2}()` + +Finally, this package also provides an `AbstractPosition` type, as well as a `Position` concrete type. +`Position` represents ownership of a financial instrument including the quantity of that financial instrument. For example, Microsoft stock (MSFT) is a financial instrument. A position could be 1,000 shares of MSFT. + +In the case of currency, `Positions.USD` would be a financial instrument and owning $1,000 would mean you own 1,000 units of the financial instrument `Positions.USD`. + +If you are building a financial application that requires adding, subtracting, multiplying and dividing currencies, then you want to use `Positions`. + +For example: +```julia +julia> using Positions + +julia> using Positions: USD, JPY + +julia> 10USD +10.00USD + +julia> 10USD+20USD +30.00USD + +julia> 5*20USD +100.00USD + +julia> 100USD/5 +20.00USD + +julia> 100USD/5USD +FixedDecimal{Int64,2}(20.00) + +julia> 100JPY/5JPY +FixedDecimal{Int64,0}(20) + +julia> 100USD+100JPY +ERROR: promotion of types Position{Cash{Currency{:USD}},FixedPointDecimals.FixedDecimal{Int64,2}} and Position{Cash{Currency{:JPY}},FixedPointDecimals.FixedDecimal{Int64,0}} failed to change any arguments +``` + +Note that algebraic operations of currency positions require the positions to be of the same financial instrument. In this case, they must be the same currency as indicated by the error in the last command above. + +See also: + +- [Markets.jl](https://github.com/JuliaFinance/Markets.jl) +- [GeneralLedgers.jl](https://github.com/JuliaFinance/GeneralLedgers.jl) + +## Data Source + +Data for this package was obtained from https://datahub.io/core/country-codes. + diff --git a/deps/build.jl b/deps/build.jl new file mode 100644 index 0000000..5bf0fd3 --- /dev/null +++ b/deps/build.jl @@ -0,0 +1,46 @@ +using JSON3 + +const src = "https://pkgstore.datahub.io/core/country-codes/country-codes_json/data/471a2e653140ecdd7243cdcacfd66608/country-codes_json.json" + +inpnam = joinpath("..", "data", "country-codes.json") +outnam = joinpath("..", "deps", "currencies.jl") + +println("Downloading currency data: ", src) +download(src, inpnam) + +const country_list = (open(inpnam) do io ; JSON3.read(io) ; end) + +const Currencies = Dict{String,Tuple{String,Int,Int,String}}() + +const SymCurr = Symbol("ISO4217-currency_alphabetic_code") +const SymUnit = Symbol("ISO4217-currency_minor_unit") +const SymName = Symbol("ISO4217-currency_name") +const SymCode = Symbol("ISO4217-currency_numeric_code") + +function genfile(io) + for country in country_list + (abbrlist = country[SymCurr]) === nothing && continue + (unitlist = country[SymUnit]) === nothing && continue + (namelist = country[SymName]) === nothing && continue + (codelist = country[SymCode]) === nothing && continue + currencies = split(abbrlist, ',') + units = split(string(unitlist), ',') + names = split(namelist, ',') + codes = split(string(codelist), ',') + + for (curr, unit, code, name) in zip(currencies, units, codes, names) + length(curr) != 3 && continue + haskey(Currencies, curr) && continue + Currencies[curr] = (curr, parse(Int, unit), parse(Int, code), string(name)) + end + end + println(io, "const Currencies = Dict(") + for (curr, val) in Currencies + println(io, " :$curr => (Currency{:$curr}, $(val[2]), $(lpad(val[3], 4)), \"$(val[4])\"),") + end + println(io, ")\n") +end + +open(outnam, "w") do io + genfile(io) +end diff --git a/src/Positions.jl b/src/Positions.jl index a2d7229..4e5d763 100644 --- a/src/Positions.jl +++ b/src/Positions.jl @@ -1,52 +1,146 @@ +""" +Positions package + +This provides the `Currency` singleton type, based on the ISO 4167 standard codes, +as well as the `FinancialInstrument` and `AbstractPosition` abstract types, +and the `Cash` and `Position` parameterized types, for dealing with all sorts of +currencies and other financial instruments in an easy fashion. + +See README.md for the full documentation + +Copyright 2019-2020, Eric Forgy, Scott P. Jones, and other contributors + +Licensed under MIT License, see LICENSE.md +""" module Positions -using FinancialInstruments, FixedPointDecimals +using FixedPointDecimals -export Currencies, Currency -export Countries, Country -export FinancialInstruments, FinancialInstrument, Cash -export Position +export Position, Currency, Currencies, Cash, AbstractPosition, FinancialInstrument +export currency, cash, unit, code, name +""" +This is an abstract type, for dealing generically with positions of financial instruments +""" abstract type AbstractPosition end +""" +This is an abstract type for `FinancialInstruments` such as `Cash`, `Stock`, +""" +abstract type FinancialInstrument end + +""" +This is a singleton type, intended to be used as a label for dispatch purposes +""" +struct Currency{Symbol} end + +# TODO: This should be an inner constructor +Currency{T}() where {T<:Symbol} = + haskey(Currencies, T) ? Currencies[T] : (Currencies[T] = new()) + +include(joinpath(dirname(pathof(@__MODULE__)), "..", "deps", "currencies.jl")) + +""" +Returns the currency type associated with this value or type +""" +function currency end + +""" +Returns the minor unit associated with this value or type +""" +function unit end + +""" +Returns the ISO 4167 code associated with this value or type +""" +function code end + +""" +Returns the ISO 4167 name associated with this value or type +""" +function name end + +currency(::Type{Currency{T}}) where {T} = T +unit(::Type{Currency{T}}) where {T} = Currencies[T][2] +code(::Type{Currency{T}}) where {T} = Currencies[T][3] +name(::Type{Currency{T}}) where {T} = Currencies[T][4] + +currency(::Currency{T}) where {T} = T +unit(c::Currency) = unit(typeof(c)) +code(c::Currency) = code(typeof(c)) +name(c::Currency) = name(typeof(c)) + +""" +`Cash` is a `FinancialInstrument`, that represents a particular currency as well +as the number of digits in the minor units, typically 0, 2, or 3 +""" +struct Cash{C<:Currency, N} <: FinancialInstrument end +Cash(::C) where {C<:Currency} = Cash{C,unit(C)}() +Cash(c::Symbol) = Cash(Currency{c}()) + +currency(::Type{Cash{C}}) where {C} = C +currency(::Cash{C}) where {C} = C +unit(::Type{Cash{C,N}}) where {C,N} = N +unit(::Cash{C,N}) where {C,N} = N +code(::Cash{C}) where {C} = code(C) +name(::Cash{C}) where {C} = name(C) + +Base.show(io::IO, ::Cash{<:Currency{T}}) where {T} = print(io, string(T)) + +# Set up short names for all of the currencies (as instances of the Cash FinancialInstruments) +for (sym, ccy) in Currencies + @eval $sym = Cash($(ccy[1]())) +end + +""" +Position represents a certain quantity of a particular financial instrument +""" struct Position{F<:FinancialInstrument,A<:Real} <: AbstractPosition amount::A + function Position(::Type{FI}, a) where {C,N,FI<:Cash{C,N}} + T = FixedDecimal{Int,N} + new{FI,T}(T(a)) + end end -Position(p,a) = Position{typeof(p),typeof(a)}(a) -Position(c::Cash{C},a) where C = Position{Cash{C},FixedDecimal{Int,Currencies.unit(C())}}(FixedDecimal{Int,Currencies.unit(C())}(a)) -Base.promote_rule(::Type{Position{F,A1}}, ::Type{Position{F,A2}}) where {F,A1,A2} = Position{F,promote_type(A1,A2)} +Position(::F, a) where {F<:FinancialInstrument} = Position(F, val) +Position{F}(val) where {F<:FinancialInstrument} = Position(F, val) + +""" +Returns the Cash financial instrument (as an instance) for a position +""" +cash(::Position{F}) where {F} = F() + +Base.promote_rule(::Type{Position{F,A1}}, ::Type{Position{F,A2}}) where {F,A1,A2} = + Position{F,promote_type(A1,A2)} Base.convert(::Type{Position{F,A}}, x::Position{F,A}) where {F,A} = x -Base.convert(::Type{Position{F,A}}, x::Position{F,<:Real}) where {F,A} = Position{F,A}(convert(A,x.amount)) +Base.convert(::Type{Position{F,A}}, x::Position{F,<:Real}) where {F,A} = + Position{F,A}(convert(A, x.amount)) -Base.:+(p1::Position{F,A},p2::Position{F,A}) where {F,A} = Position{F,A}(p1.amount+p2.amount) -Base.:+(p1::AbstractPosition,p2::AbstractPosition) = +(promote(p1,p2)...) +Base.:+(p1::Position{F1}, p2::Position{F2}) where {F1,F2} = + error("Can't add Positions of different FinancialInstruments $(F1()), $(F2())") +Base.:+(p1::Position{F}, p2::Position{F}) where {F} = Position{F}(p1.amount + p2.amount) +Base.:+(p1::AbstractPosition, p2::AbstractPosition) = +(promote(p1, p2)...) -Base.:-(p1::Position{F,A},p2::Position{F,A}) where {F,A} = Position{F,A}(p1.amount-p2.amount) -Base.:-(p1::AbstractPosition,p2::AbstractPosition) = -(promote(p1,p2)...) +Base.:-(p1::Position{F1}, p2::Position{F2}) where {F1,F2} = + error("Can't subtract Positions of different FinancialInstruments $(F1()), $(F2())") +Base.:-(p1::Position{F}, p2::Position{F}) where {F} = Position{F}(p1.amount - p2.amount) +Base.:-(p1::AbstractPosition, p2::AbstractPosition) = -(promote(p1, p2)...) -Base.:/(p1::Position{F,A},p2::Position{F,A}) where {F,A} = p1.amount/p2.amount -Base.:/(p1::AbstractPosition,p2::AbstractPosition) = /(promote(p1,p2)...) +Base.:/(p1::Position, p2::Position) = p1.amount / p2.amount +Base.:/(p1::AbstractPosition, p2::AbstractPosition) = /(promote(p1, p2)...) -# TODO: Should scalar multiplication and division respect the Position type or do normal promotion? +Base.:/(p::Position{F}, k::Real) where {F} = Position{F}(p.amount / k) -# Base.:/(p::Position{F,A},k::R) where {F,A,R<:Real} = Position{F,promote_type(A,R)}(p.amount/k) -Base.:/(p::Position{F,A},k::R) where {F,A,R<:Real} = Position{F,A}(p.amount/k) +Base.:*(k::Real, p::Position{F}) where {F} = Position{F}(p.amount * k) +Base.:*(p::Position, k::Real) = k * p -# Base.:*(k::R,p::Position{F,A}) where {F,A,R<:Real} = Position{F,promote_type(A,R)}(p.amount*k) -Base.:*(k::R,p::Position{F,A}) where {F,A,R<:Real} = Position{F,A}(p.amount*k) -Base.:*(p::Position{F,A},k::R) where {F,A,R<:Real} = k*p +Base.:*(val::Real, c::Cash) = Position{typeof(c)}(val) -Base.show(io::IO,c::Position{Cash{C}}) where C = print(io,c.amount," ",C()) -Base.show(io::IO,::MIME"text/plain",c::Position{Cash{C}}) where C = print(io,c.amount," ",C()) +Base.show(io::IO, p::Position) = print(io, p.amount, cash(p)) +Base.show(io::IO, ::MIME"text/plain", p::Position) = print(io, p.amount, cash(p)) Base.zero(::Type{Position{F,A}}) where {F,A} = Position{F,A}(zero(A)) Base.one(::Type{Position{F,A}}) where {F,A} = Position{F,A}(one(A)) -for (sym,ccy) in Currencies.list - @eval Positions begin - $sym = Position(Cash($ccy),1) - end -end - -end # module +end # module Positions diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..12888b0 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,15 @@ +using Positions +using Positions: unit, name, code, USD, PHP, HKD, SGD + +using Test + +currencies = [USD,PHP,HKD,SGD] +units = [2,2,2,2] +names = ["US Dollar","Philippine Piso","Hong Kong Dollar","Singapore Dollar"] +codes = [840,608,344,702] + +for (ccy,u,n,c) in zip(currencies,units,names,codes) + @test unit(ccy) == u + @test name(ccy) == n + @test code(ccy) == c +end