Compare commits
5 commits
def86e261c
...
c75624c249
Author | SHA1 | Date | |
---|---|---|---|
c75624c249 | |||
4317ae77b5 | |||
15837d36b2 | |||
bab60ec856 | |||
04fa3fe354 |
31 changed files with 240 additions and 131 deletions
8
Makefile
8
Makefile
|
@ -1,6 +1,10 @@
|
||||||
|
|
||||||
BINARIES=hodler
|
BINARIES=hodlerctl hodlertui hodlerweb
|
||||||
hodler_FILES=$(wildcard src/*.cr src/**/*.cr)
|
|
||||||
|
lib_FILES=$(wildcard src/lib/*.cr src/lib/**/*.cr)
|
||||||
|
hodlerctl_FILES=$(wildcard src/hodlerctl/*.cr src/hodlerctl/**/*.cr) $(lib_FILES)
|
||||||
|
hodlertui_FILES=$(wildcard src/hodlertui/*.cr src/hodlertui/**/*.cr) $(lib_FILES)
|
||||||
|
hodlerweb_FILES=$(wildcard src/hodlerweb/*.cr src/hodlerweb/**/*.cr) $(lib_FILES)
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@ shards:
|
||||||
git: https://github.com/matiasgarciaisaia/completion.git
|
git: https://github.com/matiasgarciaisaia/completion.git
|
||||||
version: 0.1.0+git.commit.780bab511917b61e7d814801fd9acd4390eda44a
|
version: 0.1.0+git.commit.780bab511917b61e7d814801fd9acd4390eda44a
|
||||||
|
|
||||||
|
tablo:
|
||||||
|
git: https://github.com/hutou/tablo.git
|
||||||
|
version: 0.9.5
|
||||||
|
|
||||||
xdg_basedir:
|
xdg_basedir:
|
||||||
git: https://github.com/shmibs/xdg_basedir.git
|
git: https://github.com/shmibs/xdg_basedir.git
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
|
|
16
shard.yml
16
shard.yml
|
@ -9,17 +9,29 @@ description: |
|
||||||
Crypto-currencies porfolio monitoring tool
|
Crypto-currencies porfolio monitoring tool
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
hodler:
|
hodlerctl:
|
||||||
main: src/main.cr
|
main: src/hodlerctl/main.cr
|
||||||
|
hodlertui:
|
||||||
|
main: src/hodlertui/main.cr
|
||||||
|
hodlerweb:
|
||||||
|
main: src/hodlerweb/main.cr
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
# Handle XDG environment variables
|
||||||
xdg_basedir:
|
xdg_basedir:
|
||||||
github: shmibs/xdg_basedir
|
github: shmibs/xdg_basedir
|
||||||
|
|
||||||
|
# Command completion for bash
|
||||||
|
# (use pull request which fixes compatibility)
|
||||||
completion:
|
completion:
|
||||||
github: matiasgarciaisaia/completion
|
github: matiasgarciaisaia/completion
|
||||||
branch: crystal-0.18
|
branch: crystal-0.18
|
||||||
# github: f/completion
|
# github: f/completion
|
||||||
|
|
||||||
|
# Text tables
|
||||||
|
tablo:
|
||||||
|
github: hutou/tablo
|
||||||
|
|
||||||
# pg:
|
# pg:
|
||||||
# github: will/crystal-pg
|
# github: will/crystal-pg
|
||||||
# version: "~> 0.5"
|
# version: "~> 0.5"
|
||||||
|
|
|
@ -1,33 +1,36 @@
|
||||||
|
|
||||||
require "./models"
|
require "option_parser"
|
||||||
require "./actions"
|
require "yaml"
|
||||||
require "./models"
|
require "colorize"
|
||||||
|
require "xdg_basedir"
|
||||||
|
require "completion"
|
||||||
|
require "log"
|
||||||
|
|
||||||
|
require "../lib/actions"
|
||||||
|
require "../lib/models"
|
||||||
|
require "../lib/types"
|
||||||
|
require "../lib/version"
|
||||||
|
|
||||||
module Hodler
|
module Hodler
|
||||||
class Cli
|
class CtlCli
|
||||||
alias Options = {
|
|
||||||
action: Action::Type,
|
|
||||||
verbose_enable: Bool,
|
|
||||||
config_file: String
|
|
||||||
}
|
|
||||||
|
|
||||||
property config : ConfigModel?
|
property config : ConfigModel?
|
||||||
property options : Options?
|
property options : GlobalOptions?
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@config = nil
|
@config = nil
|
||||||
@options = nil
|
@options = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_options(args) : Options
|
def self.parse_options(args) : GlobalOptions
|
||||||
# default values
|
# default values
|
||||||
action = Action::Type::Report
|
action = ReportAction
|
||||||
config_file = XDGBasedir.full_path("hodler/wallet.yml", :config, :read).as(String)
|
config_file = XDGBasedir.full_path("hodler/wallet.yml", :config, :read).as(String)
|
||||||
verbose_enable = true
|
verbose_enable = true
|
||||||
|
|
||||||
# parse
|
# parse
|
||||||
OptionParser.parse(args) do |parser|
|
OptionParser.parse(args) do |parser|
|
||||||
parser.banner = "Usage: #{Version::PROGRAM} [options] [commands] [arguments]"
|
parser.banner = "Usage: #{Version::PROGRAM_HODLERCTL} [options] [commands] [arguments]"
|
||||||
|
|
||||||
parser.separator
|
parser.separator
|
||||||
parser.separator "Options"
|
parser.separator "Options"
|
||||||
|
@ -53,25 +56,36 @@ module Hodler
|
||||||
parser.on "--completion", "Provide autocompletion for bash" do
|
parser.on "--completion", "Provide autocompletion for bash" do
|
||||||
# nothing here
|
# nothing here
|
||||||
end
|
end
|
||||||
complete_with Version::PROGRAM, parser
|
|
||||||
|
|
||||||
parser.separator
|
parser.separator
|
||||||
parser.separator "Commands"
|
parser.separator "Commands"
|
||||||
|
|
||||||
parser.on("report", "Compute report") do
|
parser.on("get", "Get given object") do
|
||||||
parser.banner = "Usage: #{Version::PROGRAM} list [arguments]"
|
parser.banner = "Usage: #{Version::PROGRAM_HODLERCTL} get [arguments]"
|
||||||
action = Action::Type::Report
|
|
||||||
|
parser.separator
|
||||||
|
parser.separator "Commands"
|
||||||
|
|
||||||
|
parser.on("portfolio", "Show current portfolio") do
|
||||||
|
parser.banner = "Usage: #{Version::PROGRAM_HODLERCTL} portfolio [arguments]"
|
||||||
|
action = GetPortfolioAction
|
||||||
|
end
|
||||||
|
|
||||||
|
parser.on("wallet", "Show given wallet") do
|
||||||
|
parser.banner = "Usage: #{Version::PROGRAM_HODLERCTL} portfolio [arguments]"
|
||||||
|
action = GetWalletAction
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on("tui", "Run ncurses interactive UI") do
|
# parser.on("tui", "Run ncurses interactive UI") do
|
||||||
parser.banner = "Usage: #{Version::PROGRAM} tui [arguments]"
|
# parser.banner = "Usage: #{Version::PROGRAM} tui [arguments]"
|
||||||
action = Action::Type::TextUi
|
# action = TextUiAction
|
||||||
end
|
# end
|
||||||
|
|
||||||
parser.on("web", "Run web interactive UI") do
|
# parser.on("web", "Run web interactive UI") do
|
||||||
parser.banner = "Usage: #{Version::PROGRAM} web [arguments]"
|
# parser.banner = "Usage: #{Version::PROGRAM} web [arguments]"
|
||||||
action = Action::Type::WebUi
|
# action = WebUiAction
|
||||||
end
|
# end
|
||||||
|
|
||||||
parser.separator
|
parser.separator
|
||||||
|
|
||||||
|
@ -96,18 +110,19 @@ module Hodler
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
complete_with Version::PROGRAM_HODLERCTL, parser
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
verbose_enable: verbose_enable,
|
verbose_enable: verbose_enable,
|
||||||
config_file: config_file,
|
config_file: config_file,
|
||||||
action: action
|
action: action
|
||||||
}.as(Options)
|
}.as(GlobalOptions)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_config(options)
|
def self.parse_config(options)
|
||||||
puts "Loading configuration...".colorize(:yellow) if options[:verbose_enable]
|
|
||||||
config_file = options[:config_file]
|
config_file = options[:config_file]
|
||||||
|
puts "Loading configuration... #{config_file}".colorize(:yellow) if options[:verbose_enable]
|
||||||
|
|
||||||
if ! File.exists? config_file
|
if ! File.exists? config_file
|
||||||
STDERR.puts "ERROR: Unable to read configuration file '#{config_file}'".colorize(:red)
|
STDERR.puts "ERROR: Unable to read configuration file '#{config_file}'".colorize(:red)
|
||||||
|
@ -126,14 +141,12 @@ module Hodler
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.run(args)
|
def self.run(args)
|
||||||
app = Cli.new
|
app = CtlCli.new
|
||||||
options = Cli.parse_options(args)
|
options = CtlCli.parse_options(args)
|
||||||
config = Cli.parse_config(options)
|
config = CtlCli.parse_config(options)
|
||||||
app.options = options
|
app.options = options
|
||||||
app.config = config
|
app.config = config
|
||||||
|
|
||||||
portfolio = PortfolioFactory.build(options, config)
|
|
||||||
|
|
||||||
action = ActionFactory.build(options, config)
|
action = ActionFactory.build(options, config)
|
||||||
action.perform
|
action.perform
|
||||||
|
|
||||||
|
@ -142,3 +155,6 @@ module Hodler
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Hodler::CtlCli.run(ARGV)
|
||||||
|
|
0
src/hodlertui/main.cr
Normal file
0
src/hodlertui/main.cr
Normal file
0
src/hodlerweb/main.cr
Normal file
0
src/hodlerweb/main.cr
Normal file
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
require "tablo"
|
||||||
|
|
||||||
module Hodler
|
module Hodler
|
||||||
class Action
|
class Action
|
||||||
enum Type
|
enum Type
|
||||||
|
@ -11,7 +13,8 @@ module Hodler
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(config : ConfigModel)
|
def initialize(global_options : GlobalOptions, config : ConfigModel)
|
||||||
|
@global_options = global_options
|
||||||
@config = config
|
@config = config
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,26 +32,16 @@ module Hodler
|
||||||
wallets[from_id] -= tr.from.amount
|
wallets[from_id] -= tr.from.amount
|
||||||
wallets[fee_id] -= tr.fee.amount
|
wallets[fee_id] -= tr.fee.amount
|
||||||
wallets[to_id] += tr.to.amount
|
wallets[to_id] += tr.to.amount
|
||||||
|
|
||||||
pp tr
|
|
||||||
pp wallets
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ActionFactory
|
class ActionFactory
|
||||||
def self.build(options, config)
|
def self.build(options, config)
|
||||||
action_class = Action
|
action = options[:action].new(options, config)
|
||||||
[ReportAction, WebUiAction, TextUiAction].each do |cls|
|
|
||||||
action_class = cls if cls.match(options[:action])
|
|
||||||
end
|
|
||||||
action = action_class.new(config)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "./actions/report"
|
require "./actions/*"
|
||||||
require "./actions/tui"
|
|
||||||
require "./actions/web"
|
|
||||||
|
|
12
src/lib/actions/portfolio-get.cr
Normal file
12
src/lib/actions/portfolio-get.cr
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
require "../actions"
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
class GetPortfolioAction < Action
|
||||||
|
# Display Wallet, Symbol, Held, Initial Value, Current Value, Evolution (%)
|
||||||
|
def perform
|
||||||
|
portfolio = PortfolioFactory.build(@global_options, @config)
|
||||||
|
puts portfolio.symbol_report
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
0
src/lib/actions/portfolio-set.cr
Normal file
0
src/lib/actions/portfolio-set.cr
Normal file
11
src/lib/actions/wallet-get.cr
Normal file
11
src/lib/actions/wallet-get.cr
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
require "../actions"
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
class GetWalletAction < Action
|
||||||
|
def perform
|
||||||
|
portfolio = PortfolioFactory.build(@global_options, @config)
|
||||||
|
puts portfolio.wallet_report
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
src/lib/models.cr
Normal file
22
src/lib/models.cr
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
class Model
|
||||||
|
include YAML::Serializable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require "./types"
|
||||||
|
require "./models/*"
|
||||||
|
|
||||||
|
# require "./models/deposit"
|
||||||
|
# require "./models/fee_trade"
|
||||||
|
# require "./models/portfolio"
|
||||||
|
# require "./models/ref_wallet"
|
||||||
|
# require "./models/trade"
|
||||||
|
# require "./models/transaction_trade"
|
||||||
|
# require "./models/ui_config"
|
||||||
|
# require "./models/wallet"
|
||||||
|
# require "./models/wallet_value_trade"
|
||||||
|
#
|
95
src/lib/models/portfolio.cr
Normal file
95
src/lib/models/portfolio.cr
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
|
||||||
|
require "../models"
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
class PortfolioModel < Model
|
||||||
|
|
||||||
|
getter wallets : Array(WalletModel)
|
||||||
|
|
||||||
|
def initialize()
|
||||||
|
@wallets = [] of WalletModel
|
||||||
|
@trades = [] of TradeModel
|
||||||
|
@deposits = [] of DepositModel
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_deposits(deposits : Array(DepositModel))
|
||||||
|
@deposits = deposits
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_wallets(wallets : Array(WalletModel))
|
||||||
|
@wallets = wallets
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_trades(trades : Array(TradeModel))
|
||||||
|
@trades = trades
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_wallet
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_wallet
|
||||||
|
end
|
||||||
|
|
||||||
|
def symbol_report
|
||||||
|
wallets = {} of Tuple(String,String) => Float32
|
||||||
|
@deposits.each do |deposit_wrapper|
|
||||||
|
deposit = deposit_wrapper.deposit
|
||||||
|
id = {deposit.wallet, deposit.sym}
|
||||||
|
wallets[id] ||= 0
|
||||||
|
wallets[id] += deposit.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
@trades.each do |trade_wrapper|
|
||||||
|
trade = trade_wrapper.transaction
|
||||||
|
from_id = {trade.from.wallet, trade.from.sym}
|
||||||
|
to_id = {trade.to.wallet, trade.to.sym}
|
||||||
|
fee_id = {trade.from.wallet, trade.fee.sym}
|
||||||
|
|
||||||
|
wallets[from_id] ||= 0
|
||||||
|
wallets[fee_id] ||= 0
|
||||||
|
wallets[to_id] ||= 0
|
||||||
|
wallets[from_id] -= trade.from.amount
|
||||||
|
wallets[fee_id] -= trade.fee.amount
|
||||||
|
wallets[to_id] += trade.to.amount
|
||||||
|
end
|
||||||
|
data = [] of Array(String)
|
||||||
|
wallets
|
||||||
|
.each{|key, val| data << [ key[0], key[1].to_s, "%f" % val] }
|
||||||
|
data.sort! do |x,y|
|
||||||
|
resA = x[0] <=> y[0]
|
||||||
|
resB = x[1] <=> y[1]
|
||||||
|
((resA == 0) ? resB : resA)
|
||||||
|
end
|
||||||
|
|
||||||
|
table = Tablo::Table.new(data, connectors: Tablo::CONNECTORS_SINGLE_ROUNDED) do |t|
|
||||||
|
t.add_column("Wallet") { |row| row[0] }
|
||||||
|
t.add_column("Symbol") { |row| row[1] }
|
||||||
|
t.add_column("Held", align_header: Tablo::Justify::Right, align_body: Tablo::Justify::Right) { |row| row[2] }
|
||||||
|
t.add_column("Invested") { |row| "FIXME" }
|
||||||
|
t.add_column("Cur. Price") { |row| "FIXME" }
|
||||||
|
t.add_column("Cur. Value") { |row| "FIXME" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def wallet_report
|
||||||
|
data = @wallets.map{|row| [row.name, row.type.to_s, row.refs.map{|ref| ref.sym}.join(",")] }
|
||||||
|
table = Tablo::Table.new(data, connectors: Tablo::CONNECTORS_SINGLE_ROUNDED) do |t|
|
||||||
|
t.add_column("Name") { |row| row[0] }
|
||||||
|
t.add_column("Type") { |row| row[1] }
|
||||||
|
t.add_column("Symbols", width: 40) { |row| row[2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PortfolioFactory
|
||||||
|
def self.build(options : GlobalOptions, config : ConfigModel) : PortfolioModel
|
||||||
|
portfolio = PortfolioModel.new
|
||||||
|
|
||||||
|
portfolio.import_wallets(config.wallets)
|
||||||
|
portfolio.import_trades(config.trades)
|
||||||
|
portfolio.import_deposits(config.deposits)
|
||||||
|
|
||||||
|
return portfolio
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
src/lib/types.cr
Normal file
13
src/lib/types.cr
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
require "./actions/*"
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
alias AmountT = Float64 | Int64
|
||||||
|
|
||||||
|
TABLO_CONNECTORS_EMPTY=" "
|
||||||
|
alias GlobalOptions = {
|
||||||
|
action: Action.class,
|
||||||
|
verbose_enable: Bool,
|
||||||
|
config_file: String
|
||||||
|
}
|
||||||
|
end
|
9
src/lib/version.cr
Normal file
9
src/lib/version.cr
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
module Hodler
|
||||||
|
class Version
|
||||||
|
PROGRAM_HODLERCTL = "hodlerctl"
|
||||||
|
PROGRAM_HODLERTUI = "hodlertui"
|
||||||
|
PROGRAM_HODLERWEB = "hodlerweb"
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
end
|
||||||
|
end
|
14
src/main.cr
14
src/main.cr
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
require "option_parser"
|
|
||||||
require "yaml"
|
|
||||||
require "colorize"
|
|
||||||
require "xdg_basedir"
|
|
||||||
require "completion"
|
|
||||||
|
|
||||||
require "./version"
|
|
||||||
require "./types"
|
|
||||||
require "./models"
|
|
||||||
require "./cli"
|
|
||||||
|
|
||||||
Hodler::Cli.run(ARGV)
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
require "yaml"
|
|
||||||
|
|
||||||
module Hodler
|
|
||||||
class Model
|
|
||||||
include YAML::Serializable
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
require "./types"
|
|
||||||
require "./models/config"
|
|
||||||
require "./models/deposit"
|
|
||||||
require "./models/fee_trade"
|
|
||||||
require "./models/portfolio"
|
|
||||||
require "./models/ref_wallet"
|
|
||||||
require "./models/trade"
|
|
||||||
require "./models/transaction_trade"
|
|
||||||
require "./models/ui_config"
|
|
||||||
require "./models/wallet"
|
|
||||||
require "./models/wallet_value_trade"
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
|
|
||||||
require "../models"
|
|
||||||
|
|
||||||
module Hodler
|
|
||||||
class PortfolioModel < Model
|
|
||||||
|
|
||||||
property wallets : Array(WalletModel)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@wallets = [] of WalletModel
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_wallets(wallets : Array(WalletModel))
|
|
||||||
wallets.each do |wallet_config|
|
|
||||||
wallet = WalletModel.new(wallet_config.name, wallet_config.type)
|
|
||||||
# wallet.import(wallet_config)
|
|
||||||
pp wallet_config
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_wallet
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_wallet
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class PortfolioFactory
|
|
||||||
def self.build(options : Cli::Options, config : ConfigModel)
|
|
||||||
portfolio = PortfolioModel.new
|
|
||||||
|
|
||||||
portfolio.import_wallets(config.wallets)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
module Hodler
|
|
||||||
alias AmountT = Float32 | Int32
|
|
||||||
end
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
module Hodler
|
|
||||||
class Version
|
|
||||||
PROGRAM = "hodler"
|
|
||||||
VERSION = "0.1.0"
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Add table
Reference in a new issue