coin_portfolio 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a8a032d0d880bb3634a0a36d4a326f8f8ecfd5aa
4
+ data.tar.gz: c0bb36a36153d2d0b9a3455f04323d15d1a5d51d
5
+ SHA512:
6
+ metadata.gz: 2031265f7c27bfac6e03b520a754a1c2093dd3642071ef6d1108d80ba5360f0e2328c1800e5635ec2757dc1749e1463e2cc30faa83e498b023e59816b1b8ac36
7
+ data.tar.gz: 5064e4896ed6bc79585772263cc4132500f41801c8383e024dab91b05c9d3aad4f1347d9aaa9988c2a63765e4248d98970bd7afe9d0bf72b64fcef32d6d3a3d3
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in coin_portfolio.gemspec
4
+ gemspec
@@ -0,0 +1,8 @@
1
+ Name: coin_portfolio
2
+ Copyright (c) 2016 Mário Nzualo
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,50 @@
1
+ # CoinPortfolio
2
+
3
+ CoinPortfolio calculates the gains/losses of the current cryptocurrency portfolio.
4
+
5
+ Typically when checking the evolution of the price of bitcoin you can see how much the price increased/decreased over
6
+ a period of time (e.g. day, week, year).
7
+
8
+ What if you wanted to answer the question: Would it be profitable to sell all my bitcoins now? This small library
9
+ attempts to answer that question by calculating the portfolio cost, portfolio value and the
10
+ percentage of gains/losses, if the portfolio were to be fully liquidated now.
11
+
12
+ The library tries to account for scenarios in which an account has multiple incoming and outgoing transactions with
13
+ distinct prices using the [FIFO accounting method](https://en.wikipedia.org/wiki/FIFO_and_LIFO_accounting).
14
+
15
+ This is still a work in progress.
16
+
17
+ ## Usage
18
+
19
+ The library uses the Coinbase API to:
20
+ - access the primary account for a given API key and secret
21
+
22
+ - iterate through all the account's transactions in order to get a snapshot of the distribution of the portfolio
23
+
24
+ - get the current price of the account's cryptocurrency
25
+
26
+ The API keys need to have the following permissions: `wallet:accounts:read`, `wallet:transactions:read`.
27
+
28
+ ```ruby
29
+ $ bundle exec rake console
30
+ $ api_key = "key"
31
+ $ api_secret = "secret"
32
+ $ calculator = CoinPortfolio::Calculator.new(api_key: api_key, api_secret: api_secret)
33
+ $ calculator.potential_returns
34
+ gains percentage: 25.00%
35
+ portfolio cost: €100,00
36
+ current portfolio value: €125,00
37
+ ```
38
+
39
+ ## Improvements
40
+ Until now the priority has been getting a working version therefore, there are still plenty of improvements to be made:
41
+ - API client - the code that interacts with coinbase's gem is isolated in a single class which translates
42
+ response hashes into domain objects. Yet the code is tied to coinbase's gem and would require some changes if there was
43
+ a need to integrate with another exchange. Additionally a proper handling of API related errors hasn't been
44
+ implemented.
45
+
46
+ - Multiple currencies - the code currently assumes that the user's account has a single cryptocurrency and that its
47
+ transactions are all in the same native currency. This might not be true in all cases.
48
+
49
+ ## License
50
+ MIT (c) Mário Nzualo
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
3
+
4
+ task :console do
5
+ exec "irb -r coin_portfolio -I ./lib"
6
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "coin_portfolio"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'coin_portfolio/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "coin_portfolio"
8
+ spec.version = CoinPortfolio::VERSION
9
+ spec.authors = ["Mário Nzualo"]
10
+ spec.email = ["mario.nzualo@gmail.com"]
11
+
12
+ spec.summary = %q{Calculate the gains/losses of the a crytocurrency portfolio}
13
+ spec.homepage = "https://github.com/marionzualo/coin_portfolio"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+ spec.licenses = %w(MIT)
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.13"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.4", ">= 3.4.0"
26
+ spec.add_dependency "immutable-struct", "~> 2.2", ">= 2.2.2"
27
+ spec.add_dependency "coinbase", "~> 4.1", ">= 4.1.0"
28
+ spec.add_dependency "money", "~> 6.7", ">= 6.7.1"
29
+ end
@@ -0,0 +1,9 @@
1
+ require "immutable-struct"
2
+ require "coin_portfolio/liquidation"
3
+ require "coin_portfolio/inventory"
4
+ require "coin_portfolio/inventory_item"
5
+ require "coin_portfolio/transaction"
6
+ require "coin_portfolio/exchange_client"
7
+ require "coin_portfolio/money"
8
+ require "coin_portfolio/calculator"
9
+ require "coin_portfolio/version"
@@ -0,0 +1,42 @@
1
+ module CoinPortfolio
2
+ class Calculator
3
+ def initialize(api_key:, api_secret:)
4
+ @api_key = api_key
5
+ @api_secret = api_secret
6
+ end
7
+
8
+ def potential_returns
9
+ liquidation = Liquidation.new(inventory_items)
10
+ details = liquidation.details(price)
11
+ puts "Gains percentage: #{format_percentage(details.gains_percentage)}"
12
+ puts "Portfolio cost: #{details.portfolio_cost}"
13
+ puts "Current portfolio value: #{details.current_portfolio_value}"
14
+ details
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :api_key, :api_secret
20
+
21
+ def format_percentage(percentage)
22
+ rounded = (percentage * 100).round(2)
23
+ "#{sprintf("%.2f", rounded)}%"
24
+ end
25
+
26
+ def inventory_items
27
+ Inventory.new(transactions).build
28
+ end
29
+
30
+ def transactions
31
+ client.transactions
32
+ end
33
+
34
+ def price
35
+ client.price
36
+ end
37
+
38
+ def client
39
+ @client ||= ExchangeClient.new(api_key: api_key, api_secret: api_secret)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ require "coinbase/wallet"
2
+
3
+ module CoinPortfolio
4
+ class ExchangeClient
5
+ def initialize(api_key:, api_secret:, client_factory: Coinbase::Wallet::Client)
6
+ @api_key = api_key
7
+ @api_secret = api_secret
8
+ @client_factory = client_factory
9
+ end
10
+
11
+ def price
12
+ sell_price = client.sell_price
13
+ CoinPortfolio::Money.new(amount: BigDecimal.new(sell_price["amount"]), currency: sell_price["currency"])
14
+ end
15
+
16
+ def transactions
17
+ transactions_from_client.map do |transaction|
18
+ amount = transaction["amount"]
19
+ money_amount = CoinPortfolio::Money.new(amount: BigDecimal.new(amount["amount"]).abs, currency: amount["currency"])
20
+
21
+ native_amount = transaction["native_amount"]
22
+ money_native_amount = CoinPortfolio::Money.new(amount: BigDecimal.new(native_amount["amount"]).abs, currency: native_amount["currency"])
23
+
24
+ incoming = amount["amount"][0] != "-"
25
+
26
+ CoinPortfolio::Transaction.new(amount: money_amount, native_amount: money_native_amount, incoming: incoming)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :api_key, :api_secret, :client_factory
33
+
34
+ def transactions_from_client
35
+ client.transactions(account_id, fetch_all: true)
36
+ end
37
+
38
+ def account_id
39
+ client.primary_account["id"]
40
+ end
41
+
42
+ def client
43
+ @client = client_factory.new(api_key: api_key, api_secret: api_secret)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ module CoinPortfolio
2
+ class Inventory
3
+ def initialize(transactions)
4
+ @transactions = transactions
5
+ end
6
+
7
+ def build
8
+ build_inventory_with_fifo_method
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :transactions
14
+
15
+ def build_inventory_with_fifo_method
16
+ outgoing_quantity = total_outgoing_quantity
17
+ incoming_transactions.each_with_object([]) do |transaction, inventory_items|
18
+ transaction_quantity = transaction.amount.amount
19
+ remaining_quantity = transaction_quantity - outgoing_quantity
20
+ if remaining_quantity > 0
21
+ item = InventoryItem.new(quantity: remaining_quantity, cost: transaction.price)
22
+ inventory_items.push(item)
23
+ end
24
+
25
+ outgoing_quantity = [0, outgoing_quantity - transaction_quantity].max
26
+ end
27
+ end
28
+
29
+ def incoming_transactions
30
+ transactions.select(&:incoming?)
31
+ end
32
+
33
+ def total_outgoing_quantity
34
+ transactions.reject(&:incoming?).reduce(0) do |sum, transaction|
35
+ sum + transaction.amount.amount
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module CoinPortfolio
2
+ InventoryItem = ImmutableStruct.new(:quantity, :cost)
3
+ end
@@ -0,0 +1,41 @@
1
+ module CoinPortfolio
2
+ class Liquidation
3
+ Details = ImmutableStruct.new(:portfolio_cost, :current_portfolio_value, :gains_percentage)
4
+ def initialize(inventory_items)
5
+ @inventory_items = inventory_items
6
+ end
7
+
8
+ def details(price)
9
+ current_portfolio_value = current_portfolio_value(price)
10
+ gains_percentage = (current_portfolio_value - portfolio_cost).to_f / portfolio_cost
11
+ currency = price.currency
12
+
13
+ attributes = {
14
+ portfolio_cost: Money.new(amount: portfolio_cost, currency: currency),
15
+ current_portfolio_value: Money.new(amount: current_portfolio_value, currency: currency),
16
+ gains_percentage: gains_percentage
17
+ }
18
+ Details.new(attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def portfolio_cost
24
+ inventory_items.reduce(0) do |sum, item|
25
+ sum + (item.quantity * item.cost.amount)
26
+ end
27
+ end
28
+
29
+ def current_portfolio_value(price)
30
+ total_item_quantity * price.amount
31
+ end
32
+
33
+ def total_item_quantity
34
+ inventory_items.reduce(0) do |sum, item|
35
+ sum + item.quantity
36
+ end
37
+ end
38
+
39
+ attr_reader :inventory_items
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ require "money"
2
+ I18n.enforce_available_locales = false
3
+
4
+ module CoinPortfolio
5
+ Money = ImmutableStruct.new(:amount, :currency) do
6
+ def to_s
7
+ ::Money.from_amount(amount, currency).format
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ module CoinPortfolio
2
+ class Transaction
3
+ attr_accessor :amount, :native_amount
4
+
5
+ def initialize(amount:, native_amount:, incoming:)
6
+ @amount = amount
7
+ @native_amount = native_amount
8
+ @incoming = incoming
9
+ end
10
+
11
+ def price
12
+ price_f = native_amount.amount.to_f / amount.amount
13
+ CoinPortfolio::Money.new(amount: price_f, currency: native_amount.currency)
14
+ end
15
+
16
+ def incoming?
17
+ incoming
18
+ end
19
+
20
+ private
21
+
22
+ attr_accessor :incoming
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module CoinPortfolio
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coin_portfolio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mário Nzualo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-10-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.4'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 3.4.0
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '3.4'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.4.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: immutable-struct
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.2'
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 2.2.2
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.2'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.2.2
81
+ - !ruby/object:Gem::Dependency
82
+ name: coinbase
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '4.1'
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 4.1.0
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '4.1'
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 4.1.0
101
+ - !ruby/object:Gem::Dependency
102
+ name: money
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '6.7'
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 6.7.1
111
+ type: :runtime
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.7'
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 6.7.1
121
+ description:
122
+ email:
123
+ - mario.nzualo@gmail.com
124
+ executables: []
125
+ extensions: []
126
+ extra_rdoc_files: []
127
+ files:
128
+ - ".gitignore"
129
+ - ".rspec"
130
+ - Gemfile
131
+ - LICENSE.txt
132
+ - README.md
133
+ - Rakefile
134
+ - bin/console
135
+ - bin/setup
136
+ - coin_portfolio.gemspec
137
+ - lib/coin_portfolio.rb
138
+ - lib/coin_portfolio/calculator.rb
139
+ - lib/coin_portfolio/exchange_client.rb
140
+ - lib/coin_portfolio/inventory.rb
141
+ - lib/coin_portfolio/inventory_item.rb
142
+ - lib/coin_portfolio/liquidation.rb
143
+ - lib/coin_portfolio/money.rb
144
+ - lib/coin_portfolio/transaction.rb
145
+ - lib/coin_portfolio/version.rb
146
+ homepage: https://github.com/marionzualo/coin_portfolio
147
+ licenses:
148
+ - MIT
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.5.1
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Calculate the gains/losses of the a crytocurrency portfolio
170
+ test_files: []