capwatch 0.1.13 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|