capwatch 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +5 -2
- data/README.md +6 -31
- data/Rakefile +4 -2
- data/bin/console +4 -3
- data/capwatch.gemspec +19 -17
- data/exe/capwatch +17 -5
- data/lib/capwatch.rb +15 -10
- data/lib/capwatch/cli.rb +8 -4
- data/lib/capwatch/coin.rb +51 -0
- data/lib/capwatch/console.rb +83 -61
- data/lib/capwatch/exchange.rb +21 -0
- data/lib/capwatch/fund.rb +71 -0
- data/lib/capwatch/fund_calculator.rb +31 -0
- data/lib/capwatch/fund_config.rb +69 -0
- data/lib/capwatch/providers/coin_market_cap.rb +54 -0
- data/lib/capwatch/telegram.rb +54 -17
- data/lib/capwatch/version.rb +3 -1
- data/lib/templates/cap.erb +3 -0
- data/lib/templates/watch.erb +4 -0
- metadata +24 -8
- data/funds/demo/basic.json +0 -15
- data/funds/demo/dynamic.json +0 -11
- data/funds/demo/extreme.json +0 -12
- data/lib/capwatch/calculator.rb +0 -88
- data/lib/capwatch/coinmarketcap.rb +0 -12
- data/lib/capwatch/fundparser.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5db899bf97a10c058d9b81f91795a1f2500cdd63
|
4
|
+
data.tar.gz: 7d5848e31de157b5d24b49ea9d51131e71167e18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e83306472382128dd16317851affd3aa981f9e62d29b8992aa0cd36d378a4a6b2801d9c0346f8199fd5b37dd50c0b1f7018d1f218e076220ab9cea328bd20e88
|
7
|
+
data.tar.gz: f10ea26c4fbd3e7ef0703c5ed23fbae7c887004f8a256370d8702d0dc845ca2916e9046cf08e60d3a692fbabee0b4552f3e0ee970eb73395eaf7c311a9e186a4
|
data/Gemfile
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in capwatch.gemspec
|
4
6
|
gemspec
|
5
7
|
|
6
8
|
group :test do
|
7
|
-
gem
|
9
|
+
gem "coveralls", require: false
|
10
|
+
gem "webmock"
|
8
11
|
end
|
data/README.md
CHANGED
@@ -11,39 +11,18 @@ Watch your cryptoportfolio in a console
|
|
11
11
|
## Installation
|
12
12
|
|
13
13
|
$ gem install capwatch
|
14
|
-
|
15
|
-
```bash
|
16
|
-
cat <<EOT > ~/.capwatch
|
17
|
-
{
|
18
|
-
"name": "Basic Fund",
|
19
|
-
"symbols": {
|
20
|
-
"MAID": 25452.47,
|
21
|
-
"GAME": 22253.51,
|
22
|
-
"NEO": 3826.53,
|
23
|
-
"FCT": 525.67875,
|
24
|
-
"SC": 4152770,
|
25
|
-
"DCR": 453.22,
|
26
|
-
"BTC": 8.219,
|
27
|
-
"ETH": 166.198,
|
28
|
-
"KMD": 19056.20,
|
29
|
-
"LSK": 5071.42
|
30
|
-
}
|
31
|
-
}
|
32
|
-
EOT
|
33
|
-
```
|
34
|
-
|
35
14
|
$ capwatch
|
36
15
|
|
16
|
+
Don't forget to edit `~/.capwatch` with the amount of cryptocurrencies that you hold.
|
37
17
|
|
38
18
|
## Telegram
|
39
19
|
|
40
|
-
If you want to get portfolio notifications on demand
|
20
|
+
If you want to get portfolio notifications on demand into your telegram, you'll need:
|
41
21
|
|
42
22
|
1. Create a telegram bot via [BotFather](https://core.telegram.org/bots)
|
43
23
|
2. Get the bot `token`
|
44
24
|
3. Start capwatch with the bot `token` in hand
|
45
25
|
|
46
|
-
|
47
26
|
$capwatch -e <bot_token>
|
48
27
|
|
49
28
|
Currently Capwatch supports only two commands
|
@@ -53,14 +32,10 @@ Currently Capwatch supports only two commands
|
|
53
32
|
|
54
33
|
Remember to start it on a server in a tmux window or as a daemon.
|
55
34
|
|
56
|
-
##
|
57
|
-
|
58
|
-
Fund examples can be found [here](funds/demo)
|
35
|
+
## Data Providers
|
59
36
|
|
60
|
-
|
37
|
+
- http://coinmarketcap.com
|
61
38
|
|
62
|
-
|
39
|
+
## Demo Funds
|
63
40
|
|
64
|
-
|
65
|
-
- [ ] Write Tests
|
66
|
-
- [ ] Re-factor the table class
|
41
|
+
Fund examples can be found [here](spec/fixtures/funds) which were taken from [here](www.bluemagic.info)
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require "bundler/setup"
|
5
|
+
require "capwatch"
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require 'capwatch'
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require "irb"
|
14
15
|
IRB.start(__FILE__)
|
data/capwatch.gemspec
CHANGED
@@ -1,33 +1,35 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
lib = File.expand_path(
|
4
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require
|
6
|
+
require "capwatch/version"
|
6
7
|
|
7
8
|
Gem::Specification.new do |spec|
|
8
|
-
spec.name =
|
9
|
+
spec.name = "capwatch"
|
9
10
|
spec.version = Capwatch::VERSION
|
10
|
-
spec.authors = [
|
11
|
-
spec.email = [
|
11
|
+
spec.authors = ["Nick Bugaiov"]
|
12
|
+
spec.email = ["nick@bugaiov.com"]
|
12
13
|
|
13
|
-
spec.summary =
|
14
|
-
spec.description =
|
15
|
-
spec.homepage =
|
16
|
-
spec.license =
|
14
|
+
spec.summary = "Cryptoportfolio watch"
|
15
|
+
spec.description = "Watches your cryptoportfolio"
|
16
|
+
spec.homepage = "https://cryptowatch.one"
|
17
|
+
spec.license = "MIT"
|
17
18
|
|
18
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
20
|
f.match(%r{^(test|spec|features)/})
|
20
21
|
end
|
21
22
|
|
22
|
-
spec.bindir =
|
23
|
+
spec.bindir = "exe"
|
23
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
-
spec.require_paths = [
|
25
|
+
spec.require_paths = ["lib"]
|
25
26
|
|
26
|
-
spec.add_dependency
|
27
|
-
spec.add_dependency
|
28
|
-
spec.add_dependency
|
27
|
+
spec.add_dependency "terminal-table", "~> 1.8"
|
28
|
+
spec.add_dependency "colorize", "~> 0.8"
|
29
|
+
spec.add_dependency "telegram_bot", "~> 0.0.7"
|
29
30
|
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
32
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
35
|
end
|
data/exe/capwatch
CHANGED
@@ -1,17 +1,29 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
+
require "bundler/setup"
|
5
|
+
require "capwatch"
|
4
6
|
include Capwatch
|
5
7
|
|
6
8
|
options = CLI.parse(ARGV)
|
7
9
|
|
10
|
+
trap("SIGINT") {
|
11
|
+
system("clear")
|
12
|
+
exit 130
|
13
|
+
}
|
14
|
+
|
8
15
|
if options.telegram
|
9
|
-
Telegram.new(
|
16
|
+
Telegram.new(options.telegram).start
|
10
17
|
else
|
11
18
|
loop do
|
12
|
-
|
13
|
-
|
14
|
-
|
19
|
+
config = FundConfig.new
|
20
|
+
provider = Providers::CoinMarketCap.new
|
21
|
+
fund = Fund.new(provider: provider, config: config)
|
22
|
+
system("clear")
|
23
|
+
puts fund.console_table
|
24
|
+
puts "\nHey there! This is a Demo Fund. Please set up your fund by editing the \"#{FundConfig::DEMO_CONFIG_FILE}\" in your home directory".green if config.demo?
|
15
25
|
sleep options.tick
|
16
26
|
end
|
17
27
|
end
|
28
|
+
|
29
|
+
|
data/lib/capwatch.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
-
|
2
|
-
require 'terminal-table'
|
3
|
-
require 'telegram_bot'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
3
|
+
require "colorize"
|
4
|
+
require "terminal-table"
|
5
|
+
require "telegram_bot"
|
6
|
+
|
7
|
+
require "capwatch/version"
|
8
|
+
require "capwatch/fund"
|
9
|
+
require "capwatch/fund_config"
|
10
|
+
require "capwatch/fund_calculator"
|
11
|
+
require "capwatch/providers/coin_market_cap"
|
12
|
+
require "capwatch/exchange"
|
13
|
+
require "capwatch/cli"
|
14
|
+
require "capwatch/coin"
|
15
|
+
require "capwatch/console"
|
16
|
+
require "capwatch/telegram"
|
data/lib/capwatch/cli.rb
CHANGED
@@ -1,21 +1,25 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "ostruct"
|
3
5
|
|
4
6
|
module Capwatch
|
5
7
|
class CLI
|
8
|
+
|
6
9
|
def self.parse(args)
|
7
10
|
options = OpenStruct.new
|
8
11
|
options.tick = 60 * 5
|
9
12
|
opt_parser = OptionParser.new do |opts|
|
10
|
-
opts.on(
|
13
|
+
opts.on("-t", "--tick [Integer]", Integer, "Tick Interval") do |t|
|
11
14
|
options.tick = t
|
12
15
|
end
|
13
|
-
opts.on(
|
16
|
+
opts.on("-e", "--telegram-token=", String) do |val|
|
14
17
|
options.telegram = val
|
15
18
|
end
|
16
19
|
end
|
17
20
|
opt_parser.parse!(args)
|
18
21
|
options
|
19
22
|
end
|
23
|
+
|
20
24
|
end
|
21
25
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capwatch
|
4
|
+
class Coin
|
5
|
+
attr_accessor :name, :symbol, :quantity,
|
6
|
+
:price_usd, :price_btc,
|
7
|
+
:distribution,
|
8
|
+
:percent_change_1h,
|
9
|
+
:percent_change_24h,
|
10
|
+
:percent_change_7d
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def value_btc
|
17
|
+
price_btc * quantity
|
18
|
+
end
|
19
|
+
|
20
|
+
def value_usd
|
21
|
+
price_usd * quantity
|
22
|
+
end
|
23
|
+
|
24
|
+
def value_eth
|
25
|
+
price_eth * quantity
|
26
|
+
end
|
27
|
+
|
28
|
+
def price_eth
|
29
|
+
price_btc / Exchange.rate_for("ETH")
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize
|
33
|
+
{
|
34
|
+
symbol: symbol,
|
35
|
+
name: name,
|
36
|
+
quantity: quantity,
|
37
|
+
price_usd: price_usd,
|
38
|
+
price_btc: price_btc,
|
39
|
+
distribution: distribution,
|
40
|
+
percent_change_1h: percent_change_1h,
|
41
|
+
percent_change_24h: percent_change_24h,
|
42
|
+
percent_change_7d: percent_change_7d,
|
43
|
+
value_btc: value_btc,
|
44
|
+
value_usd: value_usd,
|
45
|
+
value_eth: value_eth,
|
46
|
+
price_eth: price_eth,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
data/lib/capwatch/console.rb
CHANGED
@@ -1,91 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Capwatch
|
2
4
|
class Console
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
x[7] = format_percent(x[7])
|
11
|
-
x[8] = format_percent(x[8])
|
12
|
-
end
|
13
|
-
hash[:footer][3] = format_usd(hash[:footer][3])
|
14
|
-
hash[:footer][4] = format_btc(hash[:footer][4])
|
15
|
-
hash[:footer][5] = format_eth(hash[:footer][5])
|
16
|
-
hash[:footer][7] = format_percent(hash[:footer][7])
|
17
|
-
hash[:footer][8] = format_percent(hash[:footer][8])
|
18
|
-
hash
|
5
|
+
|
6
|
+
attr_accessor :name, :body, :totals
|
7
|
+
|
8
|
+
def initialize(name, body, totals)
|
9
|
+
@name = name
|
10
|
+
@body = format_body(body)
|
11
|
+
@totals = format_totals(totals)
|
19
12
|
end
|
20
13
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
14
|
+
def format_body(body)
|
15
|
+
JSON.parse(body).sort_by! { |e| -e["value_btc"].to_f }.map do |coin|
|
16
|
+
[
|
17
|
+
coin["name"],
|
18
|
+
Formatter.format_usd(coin["price_usd"]),
|
19
|
+
coin["quantity"],
|
20
|
+
Formatter.format_percent(coin["distribution"].to_f * 100),
|
21
|
+
Formatter.format_btc(coin["value_btc"]),
|
22
|
+
Formatter.format_eth(coin["value_eth"]),
|
23
|
+
Formatter.condition_color(Formatter.format_percent(coin["percent_change_24h"])),
|
24
|
+
Formatter.condition_color(Formatter.format_percent(coin["percent_change_7d"])),
|
25
|
+
Formatter.format_usd(coin["value_usd"])
|
26
|
+
]
|
25
27
|
end
|
26
|
-
hash[:footer][7] = condition_color(hash[:footer][7])
|
27
|
-
hash[:footer][8] = condition_color(hash[:footer][8])
|
28
|
-
hash
|
29
28
|
end
|
30
29
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
def format_totals(totals)
|
31
|
+
[
|
32
|
+
"",
|
33
|
+
"",
|
34
|
+
"",
|
35
|
+
"",
|
36
|
+
Formatter.format_btc(totals[:value_btc]),
|
37
|
+
Formatter.format_eth(totals[:value_eth]),
|
38
|
+
Formatter.condition_color(Formatter.format_percent(totals[:percent_change_24h])),
|
39
|
+
Formatter.condition_color(Formatter.format_percent(totals[:percent_change_7d])),
|
40
|
+
Formatter.format_usd(totals[:value_usd]).bold
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw_table
|
45
|
+
table = Terminal::Table.new do |t|
|
46
|
+
t.title = name.upcase
|
35
47
|
t.style = {
|
36
48
|
border_top: false,
|
37
49
|
border_bottom: false,
|
38
|
-
border_y:
|
39
|
-
border_i:
|
50
|
+
border_y: "",
|
51
|
+
border_i: "",
|
40
52
|
padding_left: 1,
|
41
53
|
padding_right: 1
|
42
54
|
}
|
43
55
|
t.headings = [
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
56
|
+
"ASSET",
|
57
|
+
"PRICE",
|
58
|
+
"QUANTITY",
|
59
|
+
"DIST %",
|
60
|
+
"BTC",
|
61
|
+
"ETH",
|
62
|
+
"24H %",
|
63
|
+
"7D %",
|
64
|
+
"USD"
|
53
65
|
]
|
54
|
-
|
66
|
+
body.each do |x|
|
55
67
|
t << x
|
56
68
|
end
|
57
69
|
t.add_separator
|
58
|
-
t.add_row
|
70
|
+
t.add_row totals
|
59
71
|
end
|
60
72
|
|
61
73
|
table
|
62
74
|
end
|
63
75
|
|
64
|
-
def self.format_usd(n)
|
65
|
-
'$' + n.round(2).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
66
|
-
end
|
67
76
|
|
68
|
-
|
69
|
-
format('฿%.2f', value)
|
70
|
-
end
|
77
|
+
class Formatter
|
71
78
|
|
72
|
-
|
73
|
-
format('Ξ%.2f', value)
|
74
|
-
end
|
79
|
+
class << self
|
75
80
|
|
76
|
-
|
77
|
-
|
78
|
-
|
81
|
+
def format_usd(n)
|
82
|
+
"$" + n.to_f.round(2).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_btc(value)
|
86
|
+
format("฿%.2f", value)
|
87
|
+
end
|
88
|
+
|
89
|
+
def format_eth(value)
|
90
|
+
format("Ξ%.2f", value)
|
91
|
+
end
|
92
|
+
|
93
|
+
def format_percent(value)
|
94
|
+
format("%.2f%", value.to_f)
|
95
|
+
end
|
96
|
+
|
97
|
+
def condition_color(value)
|
98
|
+
percent_value = value.to_f
|
99
|
+
if percent_value > 1
|
100
|
+
value.green
|
101
|
+
elsif percent_value < 0
|
102
|
+
value.red
|
103
|
+
else
|
104
|
+
value.green
|
105
|
+
end
|
106
|
+
end
|
79
107
|
|
80
|
-
def self.condition_color(value)
|
81
|
-
percent_value = value.to_f
|
82
|
-
if percent_value > 1
|
83
|
-
value.green
|
84
|
-
elsif percent_value < 0
|
85
|
-
value.red
|
86
|
-
else
|
87
|
-
value.green
|
88
108
|
end
|
89
|
-
|
109
|
+
|
110
|
+
end # class Formatter
|
111
|
+
|
90
112
|
end
|
91
113
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capwatch
|
4
|
+
class Exchange
|
5
|
+
|
6
|
+
@@rates = {}
|
7
|
+
|
8
|
+
def self.rate_for(symbol)
|
9
|
+
raise "No Exchange Rate for #{symbol}" if @@rates[symbol].nil?
|
10
|
+
@@rates[symbol]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rate(symbol, value)
|
14
|
+
@@rates[symbol] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.rates
|
18
|
+
@@rates
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capwatch
|
4
|
+
class Fund
|
5
|
+
attr_accessor :provider, :config, :coins, :positions
|
6
|
+
|
7
|
+
def initialize(provider:, config:)
|
8
|
+
@provider = provider
|
9
|
+
@config = config
|
10
|
+
@positions = config.positions
|
11
|
+
@coins = config.coins
|
12
|
+
build
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](symbol)
|
16
|
+
coins.find { |coin| coin.symbol == symbol }
|
17
|
+
end
|
18
|
+
|
19
|
+
def value_btc
|
20
|
+
coins.map(&:value_btc).sum
|
21
|
+
end
|
22
|
+
|
23
|
+
def value_usd
|
24
|
+
coins.map(&:value_usd).sum
|
25
|
+
end
|
26
|
+
|
27
|
+
def value_eth
|
28
|
+
coins.map(&:value_eth).sum
|
29
|
+
end
|
30
|
+
|
31
|
+
def percent_change_1h
|
32
|
+
coins.map { |coin| coin.percent_change_1h * coin.distribution }.sum
|
33
|
+
end
|
34
|
+
|
35
|
+
def percent_change_24h
|
36
|
+
coins.map { |coin| coin.percent_change_24h * coin.distribution }.sum
|
37
|
+
end
|
38
|
+
|
39
|
+
def percent_change_7d
|
40
|
+
coins.map { |coin| coin.percent_change_7d * coin.distribution }.sum
|
41
|
+
end
|
42
|
+
|
43
|
+
def build
|
44
|
+
calculator.assign_quantity
|
45
|
+
calculator.assign_prices
|
46
|
+
calculator.distribution
|
47
|
+
end
|
48
|
+
|
49
|
+
def calculator
|
50
|
+
@calculator ||= FundCalculator.new(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def serialize
|
54
|
+
coins.map { |coin| coin.serialize }.to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
def fund_totals
|
58
|
+
{
|
59
|
+
value_usd: value_usd,
|
60
|
+
value_btc: value_btc,
|
61
|
+
value_eth: value_eth,
|
62
|
+
percent_change_24h: percent_change_24h,
|
63
|
+
percent_change_7d: percent_change_7d
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def console_table
|
68
|
+
Console.new(name = config.name, body = serialize, totals = fund_totals).draw_table
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capwatch
|
4
|
+
class FundCalculator
|
5
|
+
|
6
|
+
attr_accessor :fund
|
7
|
+
|
8
|
+
def initialize(fund)
|
9
|
+
@fund = fund
|
10
|
+
end
|
11
|
+
|
12
|
+
def assign_quantity
|
13
|
+
fund.coins.each do |coin|
|
14
|
+
coin.quantity = fund.positions[coin.symbol]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def assign_prices
|
19
|
+
fund.coins.each do |coin|
|
20
|
+
fund.provider.update_coin(coin)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def distribution
|
25
|
+
fund.coins.each do |coin|
|
26
|
+
coin.distribution = coin.value_btc / fund.value_btc
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capwatch
|
4
|
+
class FundConfig
|
5
|
+
|
6
|
+
DEMO_CONFIG_NAME = "Your Demo Fund"
|
7
|
+
DEMO_CONFIG_FILE = "~/.capwatch"
|
8
|
+
|
9
|
+
attr_accessor :name, :positions, :config_path
|
10
|
+
|
11
|
+
def initialize(config_path = nil)
|
12
|
+
@config_path = config_path || File.expand_path(DEMO_CONFIG_FILE)
|
13
|
+
demo_config! unless config_exists?
|
14
|
+
end
|
15
|
+
|
16
|
+
def positions
|
17
|
+
@positions ||= parsed_config["symbols"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
@name ||= parsed_config["name"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def parsed_config
|
25
|
+
parse @config_path
|
26
|
+
end
|
27
|
+
|
28
|
+
def coins
|
29
|
+
positions.map do |symbol, quantity|
|
30
|
+
Coin.new do |coin|
|
31
|
+
coin.symbol = symbol
|
32
|
+
coin.quantity = quantity
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def demo?
|
38
|
+
name == DEMO_CONFIG_NAME
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def open_config(path)
|
44
|
+
File.open(path).read
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse(path)
|
48
|
+
JSON.parse open_config(path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def config_exists?
|
52
|
+
File.exist? @config_path
|
53
|
+
end
|
54
|
+
|
55
|
+
def demo_fund
|
56
|
+
file_path = File.join(__dir__, "..", "..", "spec", "fixtures", "funds", "basic.json")
|
57
|
+
demo_fund = File.expand_path(file_path)
|
58
|
+
File.open(demo_fund).read
|
59
|
+
end
|
60
|
+
|
61
|
+
def demo_config!
|
62
|
+
@config_path = File.expand_path(@config_path)
|
63
|
+
File.open(@config_path, "w") do |file|
|
64
|
+
file.write(demo_fund.gsub!("Basic Fund", DEMO_CONFIG_NAME))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "open-uri"
|
5
|
+
|
6
|
+
module Capwatch
|
7
|
+
module Providers
|
8
|
+
class CoinMarketCap
|
9
|
+
|
10
|
+
attr_accessor :body
|
11
|
+
|
12
|
+
NoCoinInProvider = Class.new(RuntimeError)
|
13
|
+
|
14
|
+
TICKER_URL = "https://api.coinmarketcap.com/v1/ticker/"
|
15
|
+
|
16
|
+
def fetched_json
|
17
|
+
response = parse(fetch)
|
18
|
+
update_rates(response)
|
19
|
+
response
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch
|
23
|
+
@body ||= open(TICKER_URL).read
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse(response)
|
27
|
+
JSON.parse(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_rates(response)
|
31
|
+
response.each do |coin_json|
|
32
|
+
Capwatch::Exchange.rate(
|
33
|
+
coin_json['symbol'],
|
34
|
+
coin_json["price_btc"].to_f
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_coin(coin)
|
40
|
+
provider_coin = fetched_json.find { |c| c["symbol"] == coin.symbol }
|
41
|
+
fail NoCoinInProvider, "No #{coin.symbol} in provider response" if provider_coin.nil?
|
42
|
+
coin.name = provider_coin["name"]
|
43
|
+
coin.price_usd = provider_coin["price_usd"].to_f
|
44
|
+
coin.price_btc = provider_coin["price_btc"].to_f
|
45
|
+
coin.percent_change_1h = provider_coin["percent_change_1h"].to_f
|
46
|
+
coin.percent_change_24h = provider_coin["percent_change_24h"].to_f
|
47
|
+
coin.percent_change_7d = provider_coin["percent_change_7d"].to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
end # class CoinMarketCap
|
51
|
+
|
52
|
+
end # module Providers
|
53
|
+
|
54
|
+
end
|
data/lib/capwatch/telegram.rb
CHANGED
@@ -1,15 +1,57 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "logger"
|
2
5
|
|
3
6
|
module Capwatch
|
7
|
+
|
4
8
|
class Telegram
|
5
9
|
|
6
|
-
attr_reader :logger, :bot, :
|
10
|
+
attr_reader :logger, :bot, :config
|
7
11
|
|
8
|
-
def initialize(
|
9
|
-
@
|
12
|
+
def initialize(token)
|
13
|
+
@config = config
|
10
14
|
@logger = Logger.new(STDOUT, Logger::DEBUG)
|
11
|
-
@logger.debug
|
15
|
+
@logger.debug "Starting telegram bot..."
|
12
16
|
@bot = TelegramBot.new(token: token)
|
17
|
+
@console_formatter = Capwatch::Console::Formatter
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_fund
|
21
|
+
config = FundConfig.new
|
22
|
+
provider = Providers::CoinMarketCap.new
|
23
|
+
Fund.new(provider: provider, config: config)
|
24
|
+
end
|
25
|
+
|
26
|
+
def template(name)
|
27
|
+
File.open(File.expand_path("#{__dir__}/../templates/#{name}")).read
|
28
|
+
end
|
29
|
+
|
30
|
+
def reply_cap
|
31
|
+
fund = new_fund
|
32
|
+
ERB.new(template("cap.erb")).result(binding)
|
33
|
+
end
|
34
|
+
|
35
|
+
def reply_watch
|
36
|
+
fund = new_fund
|
37
|
+
body = format_body(fund.serialize)
|
38
|
+
ERB.new(template("watch.erb")).result(binding)
|
39
|
+
end
|
40
|
+
|
41
|
+
def format_body(body)
|
42
|
+
JSON.parse(body).sort_by! { |e| -e["value_btc"].to_f }.map do |coin|
|
43
|
+
[
|
44
|
+
coin["name"],
|
45
|
+
Console::Formatter.format_usd(coin["price_usd"]),
|
46
|
+
coin["quantity"],
|
47
|
+
Console::Formatter.format_percent(coin["distribution"].to_f * 100),
|
48
|
+
Console::Formatter.format_btc(coin["value_btc"]),
|
49
|
+
Console::Formatter.format_eth(coin["value_eth"]),
|
50
|
+
Console::Formatter.format_percent(coin["percent_change_24h"]),
|
51
|
+
Console::Formatter.format_percent(coin["percent_change_7d"]),
|
52
|
+
Console::Formatter.format_usd(coin["value_usd"])
|
53
|
+
]
|
54
|
+
end
|
13
55
|
end
|
14
56
|
|
15
57
|
def start
|
@@ -19,26 +61,21 @@ module Capwatch
|
|
19
61
|
|
20
62
|
message.reply do |reply|
|
21
63
|
begin
|
64
|
+
|
22
65
|
case command
|
66
|
+
|
23
67
|
when /\/cap/i
|
24
|
-
|
25
|
-
reply.text = table[:footer].reject(&:empty?).join("\n")
|
68
|
+
reply.text = reply_cap
|
26
69
|
when /\/watch/i
|
27
|
-
|
28
|
-
text = [
|
29
|
-
"*#{table[:title]}*",
|
30
|
-
"\n",
|
31
|
-
table[:table].map{|x| x.join(" | ") }.join("\n"),
|
32
|
-
"\n",
|
33
|
-
table[:footer].reject(&:empty?).join(" | ")
|
34
|
-
].join("\n")
|
35
|
-
reply.text = text
|
70
|
+
reply.text = reply_watch
|
36
71
|
else
|
37
72
|
reply.text = "#{message.from.first_name}, have no idea what _#{command}_ means."
|
38
73
|
end
|
74
|
+
|
39
75
|
logger.info "sending #{reply.text.inspect} to @#{message.from.username}"
|
40
|
-
reply.parse_mode =
|
76
|
+
reply.parse_mode = "Markdown"
|
41
77
|
reply.send_with(bot)
|
78
|
+
|
42
79
|
rescue => e
|
43
80
|
logger.error e
|
44
81
|
end
|
data/lib/capwatch/version.rb
CHANGED
@@ -0,0 +1,4 @@
|
|
1
|
+
<% body.each do |array| %>
|
2
|
+
<% array.each do |value| %><%= value %> <% end %>
|
3
|
+
<% end %>
|
4
|
+
<%= @console_formatter.format_btc(fund.fund_totals[:value_btc]) %> <%= @console_formatter.format_eth(fund.fund_totals[:value_eth]) %> <%= @console_formatter.format_percent(fund.fund_totals[:percent_change_24h]) %> <%= @console_formatter.format_percent(fund.fund_totals[:percent_change_7d]) %> <%= @console_formatter.format_usd(fund.fund_totals[:value_usd]) %>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capwatch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Bugaiov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: terminal-table
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.15'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.10'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,17 +128,19 @@ files:
|
|
114
128
|
- bin/setup
|
115
129
|
- capwatch.gemspec
|
116
130
|
- exe/capwatch
|
117
|
-
- funds/demo/basic.json
|
118
|
-
- funds/demo/dynamic.json
|
119
|
-
- funds/demo/extreme.json
|
120
131
|
- lib/capwatch.rb
|
121
|
-
- lib/capwatch/calculator.rb
|
122
132
|
- lib/capwatch/cli.rb
|
123
|
-
- lib/capwatch/
|
133
|
+
- lib/capwatch/coin.rb
|
124
134
|
- lib/capwatch/console.rb
|
125
|
-
- lib/capwatch/
|
135
|
+
- lib/capwatch/exchange.rb
|
136
|
+
- lib/capwatch/fund.rb
|
137
|
+
- lib/capwatch/fund_calculator.rb
|
138
|
+
- lib/capwatch/fund_config.rb
|
139
|
+
- lib/capwatch/providers/coin_market_cap.rb
|
126
140
|
- lib/capwatch/telegram.rb
|
127
141
|
- lib/capwatch/version.rb
|
142
|
+
- lib/templates/cap.erb
|
143
|
+
- lib/templates/watch.erb
|
128
144
|
homepage: https://cryptowatch.one
|
129
145
|
licenses:
|
130
146
|
- MIT
|
data/funds/demo/basic.json
DELETED
data/funds/demo/dynamic.json
DELETED
data/funds/demo/extreme.json
DELETED
data/lib/capwatch/calculator.rb
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
module Capwatch
|
2
|
-
class Calculator
|
3
|
-
def self.fund_hash(fund, coinmarketcap_json)
|
4
|
-
table = []
|
5
|
-
|
6
|
-
title = fund['name']
|
7
|
-
symbols = fund['symbols']
|
8
|
-
fund_keys = symbols.keys
|
9
|
-
|
10
|
-
price_eth_btc = coinmarketcap_json.find do |x|
|
11
|
-
x['symbol'] == 'ETH'
|
12
|
-
end['price_usd'].to_f
|
13
|
-
|
14
|
-
filtered_response_json = coinmarketcap_json.select do |x|
|
15
|
-
fund_keys.include?(x['symbol'])
|
16
|
-
end
|
17
|
-
|
18
|
-
total_value_usd = filtered_response_json.inject(0) do |sum, n|
|
19
|
-
sum + symbols[n['symbol']] * n['price_usd'].to_f
|
20
|
-
end
|
21
|
-
|
22
|
-
total_value_btc = filtered_response_json.inject(0) do |sum, n|
|
23
|
-
sum + symbols[n['symbol']] * n['price_btc'].to_f
|
24
|
-
end
|
25
|
-
|
26
|
-
total_value_eth = filtered_response_json.inject(0) do |sum, n|
|
27
|
-
sum + symbols[n['symbol']] * n['price_usd'].to_f / price_eth_btc
|
28
|
-
end
|
29
|
-
|
30
|
-
distribution_hash = {}
|
31
|
-
|
32
|
-
fund_keys.each do |x|
|
33
|
-
x = filtered_response_json.find { |e| e['symbol'] == x }
|
34
|
-
symbol = x['symbol']
|
35
|
-
asset_name = "#{x['name']} (#{symbol})"
|
36
|
-
quant_value = symbols[symbol]
|
37
|
-
price = x['price_usd'].to_f
|
38
|
-
value_btc = quant_value * x['price_btc'].to_f
|
39
|
-
value_eth = quant_value * x['price_usd'].to_f / price_eth_btc
|
40
|
-
value_usd = quant_value * x['price_usd'].to_f
|
41
|
-
distribution_float = value_usd / total_value_usd
|
42
|
-
distribution_hash[symbol] = distribution_float
|
43
|
-
distribution = distribution_float * 100
|
44
|
-
percent_change_24h = x['percent_change_24h'].to_f || 0
|
45
|
-
percent_change_7d = x['percent_change_7d'].to_f || 0
|
46
|
-
table << [
|
47
|
-
asset_name,
|
48
|
-
price,
|
49
|
-
quant_value,
|
50
|
-
value_usd,
|
51
|
-
value_btc,
|
52
|
-
value_eth,
|
53
|
-
distribution,
|
54
|
-
percent_change_24h,
|
55
|
-
percent_change_7d
|
56
|
-
]
|
57
|
-
end
|
58
|
-
|
59
|
-
a_24h = filtered_response_json.inject(0) do |sum, n|
|
60
|
-
sum + n['percent_change_24h'].to_f * distribution_hash[n['symbol']].to_f
|
61
|
-
end
|
62
|
-
|
63
|
-
a_7d = filtered_response_json.inject(0) do |sum, n|
|
64
|
-
sum + n['percent_change_7d'].to_f * distribution_hash[n['symbol']].to_f
|
65
|
-
end
|
66
|
-
|
67
|
-
footer = [
|
68
|
-
'',
|
69
|
-
'',
|
70
|
-
'',
|
71
|
-
total_value_usd,
|
72
|
-
total_value_btc,
|
73
|
-
total_value_eth,
|
74
|
-
'',
|
75
|
-
a_24h,
|
76
|
-
a_7d
|
77
|
-
]
|
78
|
-
|
79
|
-
table.sort_by! { |e| -e[3].to_f }
|
80
|
-
table.each.with_index(1) { |e, i| e[0] = "#{i}. #{e[0]}" }
|
81
|
-
|
82
|
-
{}
|
83
|
-
.merge(title: title)
|
84
|
-
.merge(table: table)
|
85
|
-
.merge(footer: footer)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|