class Coinpare::Commands::Holdings

Constants

DEFAULT_PIE_RADIUS

Public Class Methods

new(options) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 20
def initialize(options)
  @options = options
  @pastel = Pastel.new
  @timers = Timers::Group.new
  @spinner = TTY::Spinner.new(":spinner Fetching data...",
                              format: :dots, clear: true)
  @interval = @options.fetch("watch", DEFAULT_INTERVAL).to_f
  config.set("settings", "color", value: !@options["no-color"])
end

Public Instance Methods

add_coin(input, output) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 171
def add_coin(input, output)
  prompt = create_prompt(input, output)
  context = self
  data = prompt.collect(&context.ask_coin)
  output.print cursor.up(3)
  output.print cursor.clear_screen_down
  data
end
ask_coin() click to toggle source
# File lib/coinpare/commands/holdings.rb, line 150
def ask_coin
  -> (prompt) do
    key("name").ask("What coin do you own?") do |q|
      q.default "BTC"
      q.required(true, "You need to provide a coin")
      q.validate(/\w{2,}/, "Currency can only be chars.")
      q.convert ->(coin) { coin.upcase }
    end
    key("amount").ask("What amount?") do |q|
      q.required(true, "You need to provide an amount")
      q.validate(/[\d.]+/, "Invalid amount provided")
      q.convert ->(am) { am.to_f }
    end
    key("price").ask("At what price per coin?") do |q|
      q.required(true, "You need to provide a price")
      q.validate(/[\d.]+/, "Invalid prince provided")
      q.convert ->(p) { p.to_f }
    end
  end
end
clear_output(output, lines) { || ... } click to toggle source
# File lib/coinpare/commands/holdings.rb, line 130
def clear_output(output, lines)
  output.print cursor.clear_screen_down if @options["watch"]
  yield if block_given?
  output.print cursor.up(lines) if @options["watch"]
end
create_pie_charts(raw_data, display_data) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 226
def create_pie_charts(raw_data, display_data)
  colors = %i[yellow blue green cyan magenta red]
  radius = @options["pie"].to_i > 0 ? @options["pie"].to_i : DEFAULT_PIE_RADIUS
  base   = @options.fetch("base", config.fetch("settings", "base")).upcase
  to_symbol = nil
  past_data = []
  curr_data = []

  config.fetch("holdings").each do |coin|
    coin_data = raw_data[coin["name"]][base]
    to_symbol = display_data[coin["name"]][base]["TOSYMBOL"]
    past_price = coin["amount"] * coin["price"]
    curr_price = coin["amount"] * coin_data["PRICE"]

    past_data << { name: coin["name"], value: past_price }
    curr_data << { name: coin["name"], value: curr_price }
  end

  options = {
    colors: !@options["no-color"] && colors,
    radius: radius,
    legend: {
      left: 2,
      format: "%<label>s %<name>s #{to_symbol} %<currency>s (%<percent>.0f%%)",
      precision: 2
    }
  }

  [
    TTY::Pie.new(**options.merge(data: past_data)),
    TTY::Pie.new(**options.merge(data: curr_data)),
    to_symbol
  ]
end
create_prompt(input, output) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 136
def create_prompt(input, output)
  prompt = TTY::Prompt.new(
    prefix: "[#{add_color('c', :yellow)}] ",
    input: input, output: output,
    interrupt: -> { puts; exit 1 },
    enable_color: !@options["no-color"]
  )
  prompt.on(:keypress) { |event|
    prompt.trigger(:keydown) if event.value == "j"
    prompt.trigger(:keyup) if event.value == "k"
  }
  prompt
end
display_coins(output, names, overridden_settings) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 106
def display_coins(output, names, overridden_settings)
  response = Fetcher.fetch_prices(names.join(","),
                                  overridden_settings["base"].upcase,
                                  overridden_settings)
  return unless response
  table = if @options["pie"]
            setup_table_with_pies(response["RAW"], response["DISPLAY"])
          else
            setup_table(response["RAW"], response["DISPLAY"])
          end

  @spinner.stop

  lines = banner(overridden_settings).lines.size + 1 + (table.rows_size + 3)
  clear_output(output, lines) do
    output.puts banner(overridden_settings)
    if @options["pie"]
      output.puts table.render(:unicode, padding: [0, 2])
    else
      output.puts table.render(:unicode, padding: [0, 1], alignment: :right)
    end
  end
end
execute(input: $stdin, output: $stdout) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 30
def execute(input: $stdin, output: $stdout)
  config_saved = config.exist?
  if config_saved && @options["edit"]
    editor.open(config.source_file)
    return
  elsif @options["edit"]
    output.puts "Sorry, no holdings configuration found."
    output.print "Run \""
    output.print "$ #{add_color('coinpare holdings', :yellow)}\" "
    output.puts "to setup new altfolio."
    return
  end

  config.read if config_saved

  holdings = config.fetch("holdings")
  if holdings.nil? || (holdings && holdings.empty?)
    info = setup_portfolio(input, output)
    config.merge(info)
  elsif @options["add"]
    coin_info = add_coin(input, output)
    config.append(coin_info, to: ["holdings"])
  elsif @options["remove"]
    coin_info = remove_coin(input, output)
    config.remove(*coin_info, from: ["holdings"])
  elsif @options["clear"]
    prompt = create_prompt(input, output)
    answer = prompt.yes?("Do you want to remove all holdings?")
    if answer
      config.delete("holdings")
      output.puts add_color("All holdings removed", :red)
    end
  end

  holdings = config.fetch("holdings")
  no_holdings_left = holdings.nil? || (holdings && holdings.empty?)
  if no_holdings_left
    config.delete("holdings")
  end

  # Persist current configuration
  home_file = ::File.join(Dir.home, "#{config.filename}#{config.extname}")
  file = config.source_file
  config.write(file.nil? ? home_file : file, force: true)
  if no_holdings_left
    output.puts add_color("Please add holdings to your altfolio!", :green)
    exit
  end

  @spinner.auto_spin
  settings = config.fetch("settings")
  # command options take precedence over config settings
  overridden_settings = {}
  overridden_settings["exchange"] = @options.fetch("exchange", settings.fetch("exchange"))
  overridden_settings["base"]     = @options.fetch("base", settings.fetch("base"))
  holdings = config.fetch("holdings") { [] }
  names = holdings.map { |c| c["name"] }

  if @options["watch"]
    output.print cursor.hide
    @timers.now_and_every(@interval) do
      display_coins(output, names, overridden_settings)
    end
    loop { @timers.wait }
  else
    display_coins(output, names, overridden_settings)
  end
ensure
  @spinner.stop
  if @options["watch"]
    @timers.cancel
    output.print cursor.clear_screen_down
    output.print cursor.show
  end
end
remove_coin(input, output) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 180
def remove_coin(input, output)
  prompt = create_prompt(input, output)
  holdings = config.fetch("holdings")
  data = prompt.multi_select("Which hodlings to remove?") do |menu|
    holdings.each do |holding|
      menu.choice "#{holding['name']} (#{holding['amount']})", holding
    end
  end
  output.print cursor.up(1)
  output.print cursor.clear_line
  data
end
setup_portfolio(input, output) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 193
def setup_portfolio(input, output)
  output.print "\nCurrently you have no investments setup...\n" \
               "Let's change that and create your altfolio!\n\n"

  prompt = create_prompt(input, output)
  context = self
  data = prompt.collect do
    key("settings") do
      key("base").ask("What base currency to convert holdings to?") do |q|
        q.default "USD"
        q.convert ->(b) { b.upcase }
        q.validate(/\w{3}/, "Currency code needs to be 3 chars long")
      end
      key("exchange").ask("What exchange would you like to use?") do |q|
        q.default "CCCAGG"
        q.required true
      end
    end

    while prompt.yes?("Do you want to add coin to your altfolio?")
      key("holdings").values(&context.ask_coin)
    end
  end

  lines = 4 + # intro
          2 + # base + exchange
          data["holdings"].size * 4 + 1
  output.print cursor.up(lines)
  output.print cursor.clear_screen_down

  data
end
setup_table(raw_data, display_data) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 285
def setup_table(raw_data, display_data)
  base = @options.fetch("base", config.fetch("settings", "base")).upcase
  total_buy = 0
  total = 0
  to_symbol = nil

  table = TTY::Table.new(header: [
    { value: "Coin", alignment: :left },
    "Amount",
    "Price",
    "Total Price",
    "Cur. Price",
    "Total Cur. Price",
    "Change",
    "Change%"
  ])

  config.fetch("holdings").each do |coin|
    coin_data = raw_data[coin["name"]][base]
    coin_display_data = display_data[coin["name"]][base]
    past_price = coin["amount"] * coin["price"]
    curr_price = coin["amount"] * coin_data["PRICE"]
    to_symbol = coin_display_data["TOSYMBOL"]
    change = curr_price - past_price
    arrow = pick_arrow(change)
    total_buy +=  past_price
    total += curr_price

    coin_details = [
      { value: add_color(coin["name"], :yellow), alignment: :left },
      coin["amount"],
      "#{to_symbol} #{number_to_currency(round_to(coin['price']))}",
      "#{to_symbol} #{number_to_currency(round_to(past_price))}",
      add_color("#{to_symbol} #{number_to_currency(round_to(coin_data['PRICE']))}", pick_color(change)),
      add_color("#{to_symbol} #{number_to_currency(round_to(curr_price))}", pick_color(change)),
      add_color("#{arrow} #{to_symbol} #{number_to_currency(round_to(change))}", pick_color(change)),
      add_color("#{arrow} #{round_to(percent_change(past_price, curr_price))}%", pick_color(change))
    ]
    table << coin_details
  end

  total_change = total - total_buy
  arrow = pick_arrow(total_change)

  table << [
    { value: add_color("ALL", :cyan), alignment: :left }, "-", "-",
    "#{to_symbol} #{number_to_currency(round_to(total_buy))}", "-",
    add_color("#{to_symbol} #{number_to_currency(round_to(total))}", pick_color(total_change)),
    add_color("#{arrow} #{to_symbol} #{number_to_currency(round_to(total_change))}", pick_color(total_change)),
    add_color("#{arrow} #{round_to(percent_change(total_buy, total))}%", pick_color(total_change))
  ]

  table
end
setup_table_with_pies(raw_data, display_data) click to toggle source
# File lib/coinpare/commands/holdings.rb, line 261
def setup_table_with_pies(raw_data, display_data)
  past_pie, curr_pie, to_symbol = *create_pie_charts(raw_data, display_data)

  total_change = curr_pie.total - past_pie.total
  arrow = pick_arrow(total_change)

  header_past = "Total Price (#{to_symbol} #{number_to_currency(round_to(past_pie.total))})"
  header_curr = [
    "Total Current Price (#{to_symbol} #{number_to_currency(round_to(curr_pie.total))}) ",
    add_color("#{arrow} #{to_symbol} #{number_to_currency(round_to(total_change))}", pick_color(total_change)),
  ].join

  table = TTY::Table.new(
    header: [
      { value: header_past, alignment: :center },
      { value: header_curr, alignment: :center }
    ]
  )
  past_pie.to_s.split("\n").zip(curr_pie.to_s.split("\n")).each do |past_part, curr_part|
    table << [past_part, curr_part]
  end
  table
end