Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with non-ASCII characters, improve currency load process #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions Manifest.toml

This file was deleted.

15 changes: 12 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
name = "Positions"
desc = "Financial Positions"
uuid = "81d999f0-3725-11e9-2196-fd457307c398"
authors = ["Eric Forgy <[email protected]>"]
version = "0.1.0"
authors = ["Eric Forgy <[email protected]>", "ScottPJones <[email protected]>"]
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"]
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

46 changes: 46 additions & 0 deletions deps/build.jl
Original file line number Diff line number Diff line change
@@ -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
154 changes: 124 additions & 30 deletions src/Positions.jl
Original file line number Diff line number Diff line change
@@ -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
Loading