abot-info 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bbe9530de84f99fcba646a78e0c4a61bb02ebf2e484159817721724cfe6b7733
4
+ data.tar.gz: 5f1decd77bea3d406a7abe300c9a887391afe9a7991d27561c9a5a34ea8350e4
5
+ SHA512:
6
+ metadata.gz: 4544c13838da0d2ed4af0e559b02d8c3bac17555a2cd7bdad62d0b1fcf496a13ab8e192205c537e2244775b1e4306c001eb60df1f211ae4244719265fb1565ee
7
+ data.tar.gz: b647a8cd960afb5e5bbaf739949cddfd6ab1fb4e2055ec9ab6e7228681df5e10cb76d8b4a92a528c48dad85eff855cd96275421da508f5bcdd5c8bc81bd6fcc9
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 6.1.3'
4
+ gem 'paint', '~> 2.2.1'
5
+ gem 'sqlite3', '~> 1.4.2'
6
+ gem 'terminal-table', '~> 3.0.1'
7
+ gem 'binance-ruby', '~> 1.3.1'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 w_dmitrii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Abot::Info
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/abot/info`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'abot-info'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install abot-info
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/abot-info. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+
41
+ ## Code of Conduct
42
+
43
+ Everyone interacting in the Abot::Info project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/abot-info/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/abot-info.gemspec ADDED
@@ -0,0 +1,41 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "abot/info/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'abot-info'
8
+ spec.version = Abot::Info::VERSION
9
+ spec.authors = ['w_dmitrii']
10
+ spec.email = ['wiz.work2021@gmail.com']
11
+
12
+ spec.summary = 'Abot Info'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.6')
15
+
16
+ spec.files = Dir['lib/**/*.rb', 'exe/*'] + %w[
17
+ abot-info.gemspec
18
+ Gemfile
19
+ LICENSE
20
+ Rakefile
21
+ README.md
22
+ ]
23
+
24
+ spec.test_files = Dir['spec/**/*_spec.rb']
25
+
26
+ spec.extra_rdoc_files = %w[LICENSE README.md]
27
+
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_development_dependency "bundler", "~> 1.17"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+
36
+ spec.add_dependency "activerecord", '~> 6.1.3'
37
+ spec.add_dependency "paint", '~> 2.2.1'
38
+ spec.add_dependency "sqlite3", '~> 1.4.2'
39
+ spec.add_dependency "terminal-table", '~> 3.0.1'
40
+ spec.add_dependency "binance-ruby", '~> 1.3.1'
41
+ end
data/exe/abot-info ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/abot/info'
5
+
6
+ Abot::Info.run
data/lib/abot/info.rb ADDED
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'sqlite3'
5
+ require 'terminal-table'
6
+ require 'paint'
7
+ require 'binance-ruby'
8
+
9
+ require_relative 'info/option_parser'
10
+ require_relative 'info/coin'
11
+ require_relative 'info/binance_account'
12
+ require_relative 'info/table'
13
+ require_relative 'info/version'
14
+
15
+ module Abot
16
+ module Info
17
+ class << self
18
+ def run
19
+ Info::OptionParser.instance do |parser|
20
+ parser.program_name = 'abot-info'
21
+ parser.version = Abot::Info::VERSION
22
+
23
+ parser.separator ''
24
+ parser.separator 'Application options:'
25
+
26
+ parser.add_option(
27
+ :template, '--template=TEMPLATE',
28
+ 'Шаблон (default || mobile)',
29
+ )
30
+ parser.add_option(
31
+ :add, '--add=COLUMN1,COLUMN2,COLUMN3',
32
+ "Добавить колонки:\n" \
33
+ " name: Названия монет\n" \
34
+ " name_mobile: Название монет(без изменения по дню)\n" \
35
+ " sell_up: Прибыль со сделки\n" \
36
+ " sell_price: Цена продажи\n" \
37
+ " max_price: Максимальная цена за 24ч.\n" \
38
+ " min_price: Минимальная цена за 24ч.\n" \
39
+ " current_price: Текущая цена\n" \
40
+ " next_average_price: Цена следующего усреднения\n" \
41
+ " current_quote: Текущая стоимость позиции, $\n" \
42
+ " buy_date: Дата покупки\n" \
43
+ " timer: Время с покупки\n" \
44
+ " current_profit: Текущая прибыль\n" \
45
+ " potential_profit: Потенциальная прибыль\n",
46
+ )
47
+ parser.add_option(
48
+ :del, '--del=COLUMN1,COLUMN2,COLUMN3',
49
+ "Удалить колонки:\n" \
50
+ " name: Названия монет\n" \
51
+ " name_mobile: Название монет(без изменения по дню)\n" \
52
+ " sell_up: Прибыль со сделки\n" \
53
+ " sell_price: Цена продажи\n" \
54
+ " max_price: Максимальная цена за 24ч.\n" \
55
+ " min_price: Минимальная цена за 24ч.\n" \
56
+ " current_price: Текущая цена\n" \
57
+ " next_average_price: Цена следующего усреднения\n" \
58
+ " current_quote: Текущая стоимость позиции, $\n" \
59
+ " buy_date: Дата покупки\n" \
60
+ " timer: Время с покупки\n" \
61
+ " current_profit: Текущая прибыль\n" \
62
+ " potential_profit: Потенциальная прибыль\n",
63
+ )
64
+ parser.add_option(
65
+ :db_path, '--db_path=DB_PATH',
66
+ 'Абсолютный путь до базы A-bot',
67
+ required: true
68
+ )
69
+ parser.add_option(
70
+ :symbols, '--symbols=SYMBOL1,SYMBOL2,SYMBOL3',
71
+ 'Пары для отображения курсов',
72
+ )
73
+ end.final!
74
+
75
+ require_relative 'info/database_table'
76
+
77
+ start_stats({ rate_coins: rate_coins, columns: columns })
78
+ end
79
+
80
+ def start_stats(opts)
81
+ while true do
82
+ begin
83
+ table = Table.new(opts.merge({ account: binance_account })).generate
84
+
85
+ system 'clear'
86
+ puts table
87
+ rescue StandardError => e
88
+ puts "ERROR: #{e.message}"
89
+ end
90
+
91
+ sleep 10
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def rate_coins
98
+ @rate_coins ||= Info::OptionParser.instance.options[:symbols]&.split(',') || []
99
+ end
100
+
101
+ def template
102
+ @template ||= Info::OptionParser.instance.options[:template]
103
+ end
104
+
105
+ def columns
106
+ add = Info::OptionParser.instance.options[:add]&.split(',')&.map { |m| m.to_sym } || []
107
+ del = Info::OptionParser.instance.options[:del]&.split(',')&.map { |m| m.to_sym } || []
108
+ columns = case template
109
+ when 'default'
110
+ Table::HEADINGS_COINS_TABLE_DEFAULT
111
+ when 'mobile'
112
+ Table::HEADINGS_COINS_TABLE_MOBILE
113
+ else
114
+ []
115
+ end
116
+ columns += add
117
+ columns -= del
118
+ Table.last_columns_set(columns)
119
+ end
120
+
121
+ def binance_account
122
+ BinanceAccount.new(DatabaseTable::API_KEY, DatabaseTable::SECRET_KEY)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abot
4
+ module Info
5
+ class BinanceAccount
6
+ attr_reader :api_key, :secret_key
7
+
8
+ def initialize(api_key, secret_key)
9
+ @api_key = api_key
10
+ @secret_key = secret_key
11
+ end
12
+
13
+ def free_balance(quote)
14
+ account_info[:balances].find { |f| f[:asset] == quote }.try(:[], :free).to_f
15
+ end
16
+
17
+ def current_balance_btc
18
+ @current_balance_btc ||= calculation_current_balance_btc
19
+ end
20
+
21
+ def current_balance(balance_btc, quote)
22
+ calculation_current_balance(balance_btc, quote)
23
+ end
24
+
25
+ def current_balance_wl(current_coins, quote)
26
+ calculation_current_balance_wl(current_coins, quote)
27
+ end
28
+
29
+ def percent_free_balance(balance, quote)
30
+ ((free_balance(quote) / balance) * 100).round(2)
31
+ end
32
+
33
+ def account_info
34
+ @account_info ||= Binance::Api::Account.info!(
35
+ api_key: api_key,
36
+ api_secret_key: secret_key,
37
+ )
38
+ end
39
+
40
+ def potential_balance(current_coins, trade_params)
41
+ return @potential_balance if @potential_balance
42
+
43
+ balance = current_balance(current_balance_btc, 'USDT')
44
+ quote_assets = trade_params['quote_asset'].split(' ')
45
+ potential = 0.0
46
+ btcusdt = get_symbols.find { |f| f[:symbol] == 'BTCUSDT' }
47
+
48
+ quote_assets.each do |q|
49
+ begin
50
+ pp_q = Coin.sum_potential_profit(current_coins, q) - Coin.sum_current_profit(current_coins, q)
51
+ if q == 'USDT'
52
+ potential += pp_q
53
+ elsif q == 'BTC'
54
+ potential += pp_q * btcusdt[:askPrice].to_f
55
+ elsif Coin::FIAT.include?(q)
56
+ btc = pp_q / get_symbols.find { |f| f[:symbol] == "BTC#{quote_asset}" }[:askPrice].to_f
57
+ potential += btc * btcusdt[:askPrice].to_f
58
+ else
59
+ c = pp_q / get_symbols.find { |f| f[:symbol] == "#{quote_asset}BTC" }[:askPrice].to_f
60
+ potential += btcusdt[:askPrice].to_f / c
61
+ end
62
+ rescue StandardError
63
+ puts "Ошибка подсчета потенц. баланса: #{q}"
64
+ end
65
+ end
66
+
67
+ @potential_balance ||= potential + balance
68
+ end
69
+
70
+ def symbol_info(symbol)
71
+ get_symbols.find { |f| f[:symbol] == symbol }
72
+ end
73
+
74
+ def symbol_min_price(symbol)
75
+ symbol_info(symbol).try(:[], :lowPrice).to_f
76
+ end
77
+
78
+ def symbol_max_price(symbol)
79
+ symbol_info(symbol).try(:[], :highPrice).to_f
80
+ end
81
+
82
+ private
83
+
84
+ def calculation_current_balance_btc
85
+ balance = 0.0
86
+ account_coins = {}
87
+ account_info[:balances].each do |coin|
88
+ if coin[:free].to_f != 0 || coin[:locked].to_f != 0
89
+ account_coins[coin[:asset].to_s] = coin[:free].to_f + coin[:locked].to_f
90
+ end
91
+ end
92
+ if account_coins.present?
93
+ symbols = get_symbols
94
+ btcusdt = symbols.find { |f| f[:symbol] == 'BTCUSDT' }[:askPrice].to_f
95
+ account_coins.each do |name, value|
96
+ begin
97
+ if name == 'BTC'
98
+ balance += account_coins[name]
99
+ elsif Coin::FIAT.include?(name)
100
+ coin = symbols.find { |f| f[:symbol] == "BTC#{name}" }
101
+ balance += value / coin[:askPrice].to_f
102
+ elsif symbols.find { |f| f[:symbol] == "#{name}USDT" }.nil?
103
+ coin = symbols.find { |f| f[:symbol] == "#{name}BTC" }
104
+ balance += coin[:askPrice].to_f * value
105
+ else
106
+ coin = symbols.find { |f| f[:symbol] == "#{name}USDT" }
107
+ balance += coin[:askPrice].to_f * value / btcusdt
108
+ end
109
+ rescue StandardError
110
+ puts "Ошибка при подсчете: монета #{name}"
111
+ end
112
+ end
113
+ end
114
+ balance
115
+ end
116
+
117
+ def calculation_current_balance(balance_btc, quote)
118
+ symbols = get_symbols
119
+ if Coin::FIAT.include?(quote)
120
+ coin = symbols.find { |f| f[:symbol] == "BTC#{quote}" }
121
+ balance_btc * coin.try(:[], :askPrice).to_f
122
+ else
123
+ coin = symbols.find { |f| f[:symbol] == "#{quote}BTC" }
124
+ balance_btc / coin.try(:[], :askPrice).to_f
125
+ end
126
+ end
127
+
128
+ def calculation_current_balance_wl(current_coins, quote)
129
+ balance = 0.0
130
+ current_coins = current_coins.select { |s| s.quote_asset == quote }
131
+ current_coins.each { |coin| balance += coin.current_quote }
132
+ quote_info = account_info[:balances].find { |f| f[:asset] == quote }
133
+ if quote_info.present?
134
+ balance += (quote_info.try(:[], :free).to_f + quote_info.try(:[], :locked).to_f)
135
+ end
136
+ balance
137
+ end
138
+
139
+ def get_symbols
140
+ @get_symbols ||= Binance::Api.ticker!(
141
+ type: 'daily',
142
+ api_key: api_key,
143
+ api_secret_key: secret_key
144
+ )
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decorators/coin_decorator'
4
+
5
+ module Abot
6
+ module Info
7
+ class Coin
8
+ include Abot::Info::Coin::CoinDecorator
9
+
10
+ attr_reader :base_asset, :quote_asset, :average_price, :current_price, :sell_price, :volume, :num_averaging,
11
+ :total_quote, :buy_price, :step_averaging, :tick_size, :account, :timer, :trade_params
12
+
13
+ FIAT = ['USDT', 'BUSD', 'AUD', 'BIDR', 'BRL', 'EUR', 'GBR', 'RUB', 'TRY', 'TUSD', 'USDC', 'DAI', 'IDRT', 'PAX', 'UAH', 'NGN', 'VAI', 'BVND']
14
+
15
+ def initialize(options = {})
16
+ @trade_params = options[:trade_params]
17
+ @account = options[:account]
18
+ @base_asset = options[:base_asset]
19
+ @quote_asset = options[:quote_asset]
20
+ @average_price = options[:average_price]
21
+ @current_price = options[:current_price]
22
+ @sell_price = options[:sell_price]
23
+ @volume = options[:volume]
24
+ @num_averaging = options[:num_averaging]
25
+ @total_quote = options[:total_quote]
26
+ @buy_price = options[:buy_price]
27
+ @step_averaging = options[:step_averaging]
28
+ @tick_size = options[:tick_size]
29
+ @timer = options[:timer]
30
+ end
31
+
32
+ def self.current_coins(account, trade_params)
33
+ DatabaseTable.data_coins.map do |coin|
34
+ Coin.new(
35
+ trade_params: trade_params,
36
+ account: account,
37
+ base_asset: coin['baseAsset'],
38
+ quote_asset: coin['quoteAsset'],
39
+ average_price: coin['averagePrice'].to_f,
40
+ current_price: coin['askPrice'].to_f,
41
+ sell_price: coin['sellPrice'].to_f,
42
+ volume: coin['allQuantity'].to_f,
43
+ num_averaging: (coin['numAveraging'].to_i - 1),
44
+ total_quote: coin['totalQuote'].to_f.round(2),
45
+ buy_price: coin['buyPrice'].to_f,
46
+ step_averaging: coin['stepAveraging'].to_f,
47
+ tick_size: coin['tickSize'],
48
+ timer: coin['timer'].to_s[0..9],
49
+ )
50
+ end
51
+ end
52
+
53
+ def self.available_coins_number(current_coins, data_settings, free_balance)
54
+ aver_arr = []
55
+ current_coins.each_with_index {|m| aver_arr[m.num_averaging] = ((aver_arr[m.num_averaging].to_i) + 1) }
56
+ aver_weight = 0
57
+ aver_arr.each_with_index { |e, idx| aver_weight += (e.to_i * idx) }
58
+ max_pairs = (free_balance / 100) * data_settings["max_trade_pairs"].to_i
59
+ progressive_max_pairs = max_pairs - aver_weight
60
+ progressive_max_pairs.positive? ? progressive_max_pairs.round(0) : 0
61
+ end
62
+
63
+ def self.sum_current_profit(current_coins, quote)
64
+ current_coins = current_coins.select { |s| s.quote_asset == quote }
65
+ current_coins.sum(&:current_profit)
66
+ end
67
+
68
+ def self.sum_potential_profit(current_coins, quote)
69
+ current_coins = current_coins.select { |s| s.quote_asset == quote }
70
+ current_coins.sum(&:potential_profit)
71
+ end
72
+
73
+ def percent_to_order
74
+ @percent_to_order ||= ((sell_price / current_price - 1) * 100)
75
+ end
76
+
77
+ def percent_from_min_to_average
78
+ @percent_to_min ||= ((min_price / next_average_price - 1) * 100)
79
+ end
80
+
81
+ def percent_to_max
82
+ @percent_to_max ||= ((sell_price / max_price - 1) * 100)
83
+ end
84
+
85
+ def min_price
86
+ @min_price ||= account.symbol_min_price(symbol)
87
+ end
88
+
89
+ def max_price
90
+ @max_price ||= account.symbol_max_price(symbol)
91
+ end
92
+
93
+ def current_profit
94
+ @current_profit ||= ((current_price - average_price) * volume)
95
+ end
96
+
97
+ def potential_profit
98
+ ((sell_price - average_price) * volume)
99
+ end
100
+
101
+ def current_quote
102
+ @current_quote ||= (total_quote + current_profit)
103
+ end
104
+
105
+ def sell_up
106
+ @sell_up ||= ((potential_profit / total_quote) * 100 - 0.15)
107
+ end
108
+
109
+ def next_average_price
110
+ @next_average_price ||=
111
+ (buy_price - (buy_price / 100 * (step_averaging - trade_params['buy_down'].to_f))).round(tick_round_size)
112
+ end
113
+
114
+ def next_average_price_percent
115
+ @next_average_price_percent ||= ((current_price / next_average_price - 1) * 100)
116
+ end
117
+
118
+ def name
119
+ quote_asset == 'USDT' ? base_asset : symbol
120
+ end
121
+
122
+ def symbol
123
+ base_asset + quote_asset
124
+ end
125
+
126
+ private
127
+
128
+ def tick_round_size
129
+ @tick_round_size ||= tick_size.length - 2
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abot
4
+ module Info
5
+ class DatabaseTable
6
+ DB_PATH = Info::OptionParser.instance.options[:db_path]
7
+
8
+ ActiveRecord::Base.establish_connection(
9
+ adapter: 'sqlite3',
10
+ database: DB_PATH,
11
+ )
12
+
13
+ BASE_CONNECTION = ActiveRecord::Base.connection
14
+ API_KEY_TABLE = BASE_CONNECTION.execute('SELECT * FROM api_key').freeze
15
+ API_KEY = API_KEY_TABLE.first['api'].freeze
16
+ SECRET_KEY = API_KEY_TABLE.first['secret'].freeze
17
+
18
+ class << self
19
+ def data_coins
20
+ BASE_CONNECTION.execute('SELECT * FROM symbols WHERE statusOrder IS NOT "NO_ORDER"')
21
+ end
22
+
23
+ def data_settings
24
+ BASE_CONNECTION.execute('SELECT * FROM trade_params').first
25
+ end
26
+
27
+ def data_daily_profit
28
+ BASE_CONNECTION.execute("SELECT quote, sum(profit) FROM daily_profit GROUP BY quote")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abot
4
+ module Info
5
+ class Coin
6
+ module CoinDecorator
7
+ def decorated_sell_up
8
+ rounded_sell_up = sell_up.round(2)
9
+ case num_averaging
10
+ when 1, 2
11
+ rounded_sell_up > 2 ? Paint[rounded_sell_up, :red] : rounded_sell_up
12
+ when 3, 4, 5
13
+ rounded_sell_up > 2 ? rounded_sell_up : Paint[rounded_sell_up, :red]
14
+ else
15
+ rounded_sell_up
16
+ end
17
+ end
18
+
19
+ def decorated_current_profit
20
+ rounded_current_profit = if trade_params['quote_asset'].split(' ').count > 1
21
+ "#{current_profit.round(2)} #{quote_asset}"
22
+ else
23
+ current_profit.round(2)
24
+ end
25
+ if current_profit >= 0
26
+ Paint[rounded_current_profit, :green]
27
+ else
28
+ Paint[rounded_current_profit, :red]
29
+ end
30
+ end
31
+
32
+ def decorated_num_averaging
33
+ Paint[num_averaging, (Helpers::AVERAGE_COLORS[num_averaging] || :red)]
34
+ end
35
+
36
+ def decorated_next_average_price_percent
37
+ "#{next_average_price_percent.round(2)}%"
38
+ end
39
+
40
+ def decorated_next_average_price
41
+ "#{next_average_price} (#{decorated_next_average_price_percent})"
42
+ end
43
+
44
+ def decorated_name
45
+ arrow = if account.symbol_info(symbol).try(:[], :lastPrice).to_f < account.symbol_info(symbol).try(:[], :askPrice).to_f
46
+ Paint[' ↑', :green]
47
+ elsif account.symbol_info(symbol).try(:[], :lastPrice).to_f > account.symbol_info(symbol).try(:[], :askPrice).to_f
48
+ Paint[' ↓', :red]
49
+ else
50
+ ''
51
+ end
52
+ "#{name}(#{account.symbol_info(symbol).try(:[], :priceChangePercent)}%)#{arrow}"
53
+ end
54
+
55
+ def decorated_name_mobile
56
+ name
57
+ end
58
+
59
+ def decorated_sell_price
60
+ sell_price
61
+ end
62
+
63
+ def decorated_current_price
64
+ "#{current_price} (#{percent_to_order.round(2)}%)"
65
+ end
66
+
67
+ def decorated_min_price
68
+ "#{min_price} (#{percent_from_min_to_average.round(2)}%)"
69
+ end
70
+
71
+ def decorated_max_price
72
+ "#{max_price} (#{percent_to_max.round(2)}%)"
73
+ end
74
+
75
+ def decorated_current_quote
76
+ current_quote.round(2)
77
+ end
78
+
79
+ def decorated_potential_profit
80
+ if trade_params['quote_asset'].split(' ').count > 1
81
+ "#{potential_profit.round(2)} #{quote_asset}"
82
+ else
83
+ potential_profit.round(2)
84
+ end
85
+ end
86
+
87
+ def decorated_buy_date
88
+ Time.at(timer.to_s[0..9].to_f).strftime('%d %h %H:%M')
89
+ rescue StandardError
90
+ '-'
91
+ end
92
+
93
+ def decorated_timer
94
+ medidas = ["г", "мес", "д", "ч", "м", "c"]
95
+ array = [1970, 1, 1, 0, 0, 0]
96
+ text = ''
97
+ Time.at(Time.now.to_i - timer.to_s[0..9].to_f).utc.to_a.take(6).reverse.each_with_index do |k, i|
98
+ case i
99
+ when 0, 1, 2
100
+ next if text.blank? && (k - array[i]).zero?
101
+
102
+ text = "#{text} #{k - array[i]}#{medidas[i]}"
103
+ when 3
104
+ next_text = (k - array[i]).to_s.rjust(2, '0')
105
+ text = "#{text} #{next_text}"
106
+ when 4
107
+ next_text = (k - array[i]).to_s.rjust(2, '0')
108
+ text = "#{text}:#{next_text}"
109
+ end
110
+ end
111
+ text
112
+ rescue StandardError
113
+ '-'
114
+ end
115
+
116
+ def decorated_cell(name)
117
+ str = public_send("decorated_#{name}")
118
+ value = Paint[str, (Helpers::AVERAGE_COLORS[num_averaging] || :red)]
119
+ row = { value: value }
120
+ row[:alignment] = :right if name == :timer
121
+ row
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abot
4
+ module Info
5
+ # helper
6
+ module Helpers
7
+ AVERAGE_COLORS = %i[
8
+ white
9
+ green
10
+ cyan
11
+ yellow
12
+ magenta
13
+ ].freeze
14
+
15
+ def self.daily_profit_str
16
+ profit = 'Профит сегодня:'
17
+ DatabaseTable.data_daily_profit.each do |c|
18
+ tick_size = (c['quote'] == 'BTC' ? 7 : 2)
19
+ profit += Paint[" #{c['sum(profit)'].round(tick_size)} #{c['quote']};", :green]
20
+ end
21
+ profit
22
+ end
23
+
24
+ def self.balance_str(account, current_coins, trade_params)
25
+ quote_assets = trade_params['quote_asset'].split(' ')
26
+ result = ''
27
+ balance_btc = account.current_balance_btc
28
+ quote_assets.each_with_index do |quote, idx|
29
+ result += "\n" unless idx.zero?
30
+ tick_size = (quote == 'BTC' ? 6 : 2)
31
+
32
+ balance = quote == 'BTC' ? balance_btc : account.current_balance(balance_btc, quote)
33
+ balance_wl = account.current_balance_wl(current_coins, quote)
34
+
35
+ cb = "Б: #{Paint["#{balance.round(tick_size)} ", :green]}"
36
+ cbwl = "Б(WL): #{Paint["#{balance_wl.round(tick_size)} ", :green]}"
37
+ fb = "С: #{Paint["#{account.free_balance(quote).round(tick_size)} " \
38
+ "(#{account.percent_free_balance(balance_wl, quote)}%)", :green]} "
39
+ result += (quote + ' ' + cb + cbwl + fb)
40
+ end
41
+ result
42
+ rescue StandardError
43
+ ''
44
+ end
45
+
46
+ def self.potential_balance_str(account, current_coins, trade_params)
47
+ "П: #{Paint["#{account.potential_balance(current_coins, trade_params).round(2)} USDT", :green]}"
48
+ end
49
+
50
+ def self.symbol_price_str(account, symbol_name)
51
+ symbol = account.symbol_info(symbol_name.to_s)
52
+ return '' if symbol.nil?
53
+
54
+ percent_color = symbol[:priceChangePercent].to_f.positive? ? :green : :red
55
+ "#{symbol_name}: " + Paint["#{symbol[:askPrice].to_f} (#{symbol[:priceChangePercent]}%)", percent_color]
56
+ end
57
+
58
+ def self.check_abot
59
+ Paint['WARNING: ПРОВЕРЬТЕ БОТА !!!', :black, :red] + "\n" if (Time.now - File.mtime(DatabaseTable::DB_PATH)) > 10
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'ostruct'
5
+
6
+ module Abot
7
+ module Info
8
+ # Класс раширяет возможности стандартного gem'а
9
+ #
10
+ class OptionParser < OptionParser
11
+ class Error < StandardError; end
12
+
13
+ Option = Struct.new(:type, :value, :required)
14
+ private_class_method :new
15
+
16
+ private
17
+
18
+ def initialize
19
+ super
20
+ @options = {}
21
+ end
22
+
23
+ protected
24
+
25
+ def add_help_about_envs
26
+ envs = @options.select { |_, option| option[:type] == :environment }
27
+
28
+ return if envs.empty?
29
+
30
+ separator ''
31
+ separator 'Accessed environments:'
32
+ envs.each do |key, v|
33
+ separator ' ' * 4 + [key.to_s.upcase, v[:value]].join('=')
34
+ end
35
+ end
36
+
37
+ def show_help!
38
+ show_help = @options.fetch(
39
+ :help,
40
+ Option.new(:option, false, false)
41
+ )
42
+
43
+ return unless show_help[:value]
44
+
45
+ add_help_about_envs
46
+
47
+ print help
48
+ # получение справочной информации
49
+ Process.exit! unless ENV.fetch('RUN_TEST', nil)
50
+ end
51
+
52
+ def show_version!
53
+ show_version = @options.fetch(
54
+ :version,
55
+ Option.new(:option, false, false)
56
+ )
57
+
58
+ return unless show_version[:value]
59
+
60
+ puts ver
61
+ # аналогично методу #show_help!
62
+ Process.exit! unless ENV.fetch('RUN_TEST', nil)
63
+ end
64
+
65
+ def validate!
66
+ required_options = @options.select do |_, option|
67
+ option[:required] && option[:value].nil?
68
+ end
69
+
70
+ return if required_options.empty?
71
+
72
+ raise Error, <<~ERROR
73
+ Having problems:
74
+ #{
75
+ required_options.map do |key, option|
76
+ case option[:type]
77
+ when :environment
78
+ "- environment '#{key.to_s.upcase}' is required"
79
+ when :option
80
+ "- option '#{key}' is required"
81
+ end
82
+ end.join("\n")
83
+ }
84
+ ERROR
85
+ end
86
+
87
+ public
88
+
89
+ def self.instance
90
+ @instance ||= new
91
+
92
+ yield @instance if block_given?
93
+
94
+ @instance
95
+ end
96
+
97
+ # Обрабатывает переменную среды окружения и регистрирует его в опциях
98
+ #
99
+ # Указанный ключ в аргументе метода, при поиске переменной среды окружения
100
+ # будет преобразован сл. образом: `:env_key => "ENV_KEY"`
101
+ #
102
+ # @param key [Symbol]
103
+ #
104
+ # @param default [Object]
105
+ #
106
+ # @param required [FalseClass,TrueClass]
107
+ #
108
+ def add_env(key, default: nil, required: false)
109
+ raise Error, 'call after parse' if frozen?
110
+
111
+ @options[key] = Option.new(
112
+ :environment,
113
+ environment(key.to_s.upcase) || default,
114
+ required
115
+ )
116
+ end
117
+
118
+ # Добавляет обработчик опций командной строки
119
+ #
120
+ # @param key [Symbol]
121
+ #
122
+ # @param args [Array]
123
+ #
124
+ # @param default [Object]
125
+ #
126
+ # @param required [FalseClass,TrueClass]
127
+ #
128
+ def add_option(key, *args, default: nil, required: false)
129
+ raise Error, 'call after parse' if frozen?
130
+
131
+ @options[key] = Option.new(
132
+ :option,
133
+ default,
134
+ required
135
+ )
136
+
137
+ on(*args) { |value| @options[key][:value] = value }
138
+ end
139
+
140
+ # Выполняет обработку входящих аргументов и читает конфигурационый файл
141
+ #
142
+ # @return [void]
143
+ #
144
+ def final!(args = ARGV)
145
+ raise Error, 'duplicate call #final!' if frozen?
146
+
147
+ parse args
148
+ load @options[:config_file] ? @options[:config_file][:value] : nil
149
+ # заполняет значение до заморозки экземпляра класса
150
+ @banner = "Usage: #{program_name} [options]" if @banner.nil?
151
+
152
+ freeze
153
+
154
+ show_help!
155
+ show_version!
156
+ validate!
157
+ end
158
+
159
+ # Возвращает полученные в результате работы парсера опции
160
+ #
161
+ # @param keys_filter [Array<Symbol>] исключает отсуствующие ключи
162
+ #
163
+ # @return [Hash]
164
+ #
165
+ def options(keys_filter = [])
166
+ raise Error, 'call before #final!' unless frozen?
167
+
168
+ result = @options.each_with_object({}) do |option, acc|
169
+ acc[option[0]] = option[1][:value] unless option[1][:value].nil?
170
+ end
171
+
172
+ return result if keys_filter.empty?
173
+
174
+ result.select { |k, _| keys_filter.include? k }
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'helpers'
3
+
4
+ module Abot
5
+ module Info
6
+ class Table < Terminal::Table
7
+ include Abot::Info::Helpers
8
+
9
+ attr_reader :current_coins, :account, :rate_coins, :columns, :trade_params
10
+
11
+ ALL_HEADINGS = {
12
+ name: 'Coin',
13
+ name_mobile: 'Coin',
14
+ sell_up: "sell\nup",
15
+ sell_price: 'Ордер',
16
+ max_price: 'Максимум 24h',
17
+ min_price: 'Минимум 24h',
18
+ current_price: 'Тек.цена',
19
+ next_average_price: "Следующее\nусреднение",
20
+ current_quote: "Тек.\nстоим.",
21
+ current_profit: "Тек.\nпрофит",
22
+ potential_profit: "Пот.\nпрофит",
23
+ buy_date: "Дата покупки",
24
+ timer: 'Время',
25
+ }.freeze
26
+ HEADINGS_COINS_TABLE_DEFAULT = %i[
27
+ name
28
+ sell_up
29
+ sell_price
30
+ max_price
31
+ current_price
32
+ min_price
33
+ next_average_price
34
+ current_quote
35
+ buy_date
36
+ timer
37
+ current_profit
38
+ potential_profit
39
+ ].freeze
40
+ HEADINGS_COINS_TABLE_MOBILE = %i[
41
+ name_mobile
42
+ sell_up
43
+ sell_price
44
+ current_price
45
+ next_average_price
46
+ ].freeze
47
+ COLUMN_WITH_TOTAL = %i[
48
+ current_profit
49
+ potential_profit
50
+ ].freeze
51
+
52
+ def initialize(options)
53
+ @account = options[:account]
54
+ @columns = options[:columns] || HEADINGS_COINS_TABLE_DEFAULT
55
+ @rate_coins = options.fetch(:rate_coins, [])
56
+ @trade_params ||= DatabaseTable.data_settings
57
+ @current_coins = Coin.current_coins(account, trade_params)
58
+
59
+
60
+ options = {
61
+ headings: columns.map { |h| { value: ALL_HEADINGS[h], alignment: :center } },
62
+ style: { border_x: '=' }
63
+ }
64
+ super options
65
+ end
66
+
67
+ def self.last_columns_set(col)
68
+ col = col.map { |m| ALL_HEADINGS.keys.include?(m) ? m : nil }.compact
69
+ COLUMN_WITH_TOTAL.each { |e| col << col.delete(e) }
70
+ col.compact
71
+ end
72
+
73
+ def generate
74
+ sorted_current_coins.each do |coin|
75
+ row = columns.map do |m|
76
+ coin.decorated_cell(m)
77
+ end
78
+ add_row row
79
+ end
80
+ add_separator
81
+ add_row row_total
82
+ self
83
+ end
84
+
85
+ def total_averages
86
+ av_arr = []
87
+ current_coins.each { |coin| av_arr[coin.num_averaging] = av_arr[coin.num_averaging].to_i + 1 }
88
+ count = av_arr.compact.sum
89
+ av_arr = av_arr.map.with_index do |av, idx|
90
+ Paint["#{idx}: #{av.to_i}", (Helpers::AVERAGE_COLORS[idx] || :red)] if av.to_i != 0
91
+ end.compact
92
+ av_arr << "∑: #{count}"
93
+ # av_arr << "M: #{available_coins_number}" if available_coins_number
94
+ av_arr.join("\n")
95
+ end
96
+
97
+ def info_str
98
+ "#{Helpers.check_abot}" \
99
+ "#{Helpers.daily_profit_str}\n" \
100
+ "#{Helpers.balance_str(account, current_coins, trade_params)}\n" \
101
+ "#{Helpers.potential_balance_str(account, current_coins, trade_params)}\n" \
102
+ "#{rate_coins_str}"
103
+ end
104
+
105
+ def rate_coins_str
106
+ result = ''
107
+ rate_coins.each_with_index do |c, idx|
108
+ number = colspan / 2
109
+ number += 1 if number.zero?
110
+
111
+ result += if idx != 0 && (idx % number).zero?
112
+ "\n"
113
+ elsif idx != 0
114
+ ' '
115
+ else
116
+ ''
117
+ end
118
+ str = Helpers.symbol_price_str(account, c.upcase)
119
+ str.slice!('USDT')
120
+ result += str
121
+ end
122
+ result
123
+ end
124
+
125
+ def available_coins_number
126
+ @available_coins_number ||= Coin.available_coins_number(
127
+ current_coins,
128
+ trade_params,
129
+ account.percent_free_balance(current_coins)
130
+ )
131
+ end
132
+
133
+ def colspan
134
+ (columns - COLUMN_WITH_TOTAL).count - 1
135
+ end
136
+
137
+ def row_total
138
+ row_total = [
139
+ total_averages,
140
+ { value: info_str, colspan: colspan }
141
+ ]
142
+ quote_assets = trade_params['quote_asset'].split(' ')
143
+ cell_current_profit = ''
144
+ cell_potential_profit = ''
145
+ quote_assets.each do |qa|
146
+ if quote_assets.count > 1
147
+ cell_current_profit += "#{Coin.sum_current_profit(current_coins, qa).round(2)} #{qa}\n"if columns.include?(:current_profit)
148
+ cell_potential_profit += "#{Coin.sum_potential_profit(current_coins, qa).round(2)} #{qa}\n" if columns.include?(:potential_profit)
149
+ else
150
+ cell_current_profit += Coin.sum_current_profit(current_coins, qa).round(2).to_s if columns.include?(:current_profit)
151
+ cell_potential_profit += Coin.sum_potential_profit(current_coins, qa).round(2).to_s if columns.include?(:potential_profit)
152
+ end
153
+ end
154
+ row_total << cell_current_profit if columns.include?(:current_profit)
155
+ row_total << cell_potential_profit if columns.include?(:potential_profit)
156
+ row_total
157
+ end
158
+
159
+ def sorted_current_coins
160
+ current_coins.sort_by(&:percent_to_order)
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abot
4
+ module Info
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.describe Abot::Info do
2
+ it "has a version number" do
3
+ expect(Abot::Info::VERSION).not_to be nil
4
+ end
5
+
6
+ it "does something useful" do
7
+ expect(false).to eq(true)
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: abot-info
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - w_dmitrii
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-07-20 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.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
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.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 6.1.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 6.1.3
69
+ - !ruby/object:Gem::Dependency
70
+ name: paint
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.2.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.2.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.4.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.4.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: terminal-table
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 3.0.1
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.0.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: binance-ruby
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.3.1
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.3.1
125
+ description:
126
+ email:
127
+ - wiz.work2021@gmail.com
128
+ executables:
129
+ - abot-info
130
+ extensions: []
131
+ extra_rdoc_files:
132
+ - LICENSE
133
+ - README.md
134
+ files:
135
+ - Gemfile
136
+ - LICENSE
137
+ - README.md
138
+ - Rakefile
139
+ - abot-info.gemspec
140
+ - exe/abot-info
141
+ - lib/abot/info.rb
142
+ - lib/abot/info/binance_account.rb
143
+ - lib/abot/info/coin.rb
144
+ - lib/abot/info/database_table.rb
145
+ - lib/abot/info/decorators/coin_decorator.rb
146
+ - lib/abot/info/helpers.rb
147
+ - lib/abot/info/option_parser.rb
148
+ - lib/abot/info/table.rb
149
+ - lib/abot/info/version.rb
150
+ - spec/abot/statistics_spec.rb
151
+ homepage:
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 2.6.6
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.0.9
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: Abot Info
174
+ test_files:
175
+ - spec/abot/statistics_spec.rb