stockfolio 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/stockfolio +6 -0
- data/lib/stockfolio.rb +30 -0
- data/lib/stockfolio/portfolio.rb +10 -0
- data/lib/stockfolio/runner.rb +194 -0
- data/lib/stockfolio/transaction.rb +31 -0
- data/lib/stockfolio/version.rb +3 -0
- data/lib/stockfolio/watchlist.rb +8 -0
- data/lib/stockfolio/web.rb +16 -0
- metadata +76 -0
data/bin/stockfolio
ADDED
data/lib/stockfolio.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'data_mapper' # requires all the gems listed above
|
2
|
+
require 'dm-migrations'
|
3
|
+
|
4
|
+
require_relative 'stockfolio/portfolio'
|
5
|
+
require_relative 'stockfolio/transaction'
|
6
|
+
require_relative 'stockfolio/watchlist'
|
7
|
+
|
8
|
+
module StockFolio
|
9
|
+
autoload :Runner, 'stockfolio/runner'
|
10
|
+
autoload :Web, 'stockfolio/web'
|
11
|
+
end
|
12
|
+
|
13
|
+
#DataMapper::Logger.new($stdout, :debug)
|
14
|
+
|
15
|
+
rcfile = ENV['STOCKFOLIO_YML'] || Dir.home + '/.stockfolio.yml'
|
16
|
+
|
17
|
+
if File.exists?(rcfile)
|
18
|
+
config = YAML::load(File.open(rcfile))
|
19
|
+
ENV['STOCKFOLIO_DB'] = config['db'] || nil
|
20
|
+
end
|
21
|
+
|
22
|
+
dbfile = ENV['STOCKFOLIO_DB'] || Dir.home + '/.stockfolio.db'
|
23
|
+
dbfile = File.expand_path(dbfile)
|
24
|
+
|
25
|
+
# A Sqlite3 connection to a persistent database
|
26
|
+
DataMapper.setup(:default, "sqlite://#{dbfile}")
|
27
|
+
|
28
|
+
DataMapper.finalize
|
29
|
+
DataMapper.auto_upgrade!
|
30
|
+
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'boson/runner'
|
2
|
+
require 'hirb'
|
3
|
+
|
4
|
+
class StockFolio::Runner < Boson::Runner
|
5
|
+
|
6
|
+
def self.common_transaction_options
|
7
|
+
option :portfolio, :type => :string, :desc => 'Portfolio name', :required => true
|
8
|
+
option :symbol, :type=>:string, :desc => 'Symbol of stock', :required => true
|
9
|
+
option :price, :type => :numeric, :desc => 'Price paid', :required => true
|
10
|
+
option :quantity, :type => :numeric, :desc => 'Quantity', :required => true
|
11
|
+
option :fee, :type => :numeric, :desc => 'Transaction fee'
|
12
|
+
option :date, :type => :string, :desc => 'Transaction date'
|
13
|
+
end
|
14
|
+
|
15
|
+
def quote(symbol)
|
16
|
+
quote = StockFolio::Web.quote(symbol)
|
17
|
+
print_quotes(quote)
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'List porfolios'
|
21
|
+
def portfolios
|
22
|
+
q = Portfolio.all
|
23
|
+
portfolios = []
|
24
|
+
q.each do |p|
|
25
|
+
portfolios << {
|
26
|
+
:name => p.name,
|
27
|
+
:created_at => p.created_at
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
puts Hirb::Helpers::Table.render(portfolios, :fields => [:name, :created_at])
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Current positions'
|
35
|
+
def positions(name = nil)
|
36
|
+
portfolios = []
|
37
|
+
if nil == name
|
38
|
+
portfolios = Portfolio.all
|
39
|
+
else
|
40
|
+
portfolios = Portfolio.all(:name => name)
|
41
|
+
end
|
42
|
+
|
43
|
+
positions = {}
|
44
|
+
portfolios.each do |portfolio|
|
45
|
+
#puts "Positions for #{portfolio.name}"
|
46
|
+
portfolio.transactions.each do |transaction|
|
47
|
+
#puts "#{transaction.order_name} #{transaction.quantity} #{transaction.symbol}"
|
48
|
+
if positions[transaction.symbol]
|
49
|
+
positions[transaction.symbol][:quantity] = positions[transaction.symbol][:quantity] + transaction.quantity
|
50
|
+
#if transaction.quantity > 0
|
51
|
+
positions[transaction.symbol][:cost] = positions[transaction.symbol][:cost] + transaction.quantity * transaction.price + transaction.real_fee
|
52
|
+
#end
|
53
|
+
positions[transaction.symbol][:balance] = positions[transaction.symbol][:balance] + transaction.quantity * transaction.price + transaction.real_fee
|
54
|
+
else
|
55
|
+
positions[transaction.symbol] = {
|
56
|
+
:symbol => transaction.symbol,
|
57
|
+
:cost => transaction.quantity * transaction.price + transaction.real_fee,
|
58
|
+
:balance => transaction.quantity * transaction.price,
|
59
|
+
:quantity => transaction.quantity
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get current prices
|
66
|
+
quotes = StockFolio::Web.quote(positions.keys.join(","))
|
67
|
+
quotes.each do |q|
|
68
|
+
symbol = "#{q["e"]}:#{q["t"]}"
|
69
|
+
positions[symbol][:l] = q["l"]
|
70
|
+
positions[symbol][:c] = q["c"]
|
71
|
+
positions[symbol][:cp] = q["cp"]
|
72
|
+
positions[symbol][:value] = positions[symbol][:quantity] * q["l"].to_f
|
73
|
+
end
|
74
|
+
|
75
|
+
pos = []
|
76
|
+
positions.each do |symbol,position|
|
77
|
+
p = {}
|
78
|
+
p["Symbol"] = symbol.split(":")[1]
|
79
|
+
p["Last Price"] = "$#{position[:l].to_f.round(2)}"
|
80
|
+
p["Change"] = "#{position[:c]} (#{position[:cp]}%)"
|
81
|
+
|
82
|
+
if position[:quantity] > 0
|
83
|
+
p["Day's Gain"] = (position[:quantity] * position[:c].to_f).round(2)
|
84
|
+
p["Shares"] = position[:quantity]
|
85
|
+
p["Cost Basis"] = "$#{position[:cost].round(2)}"
|
86
|
+
p["Market Value"] = "$#{position[:value].round(2)}"
|
87
|
+
p["Gain"] = "$#{(position[:value] - position[:cost]).round(2)}"
|
88
|
+
p["Gain %"] = "#{(100.0 * (position[:value] - position[:cost]) / position[:cost]).round(1)}%"
|
89
|
+
else
|
90
|
+
p["Gain"] = "$#{(0 - position[:balance]).round(2)}"
|
91
|
+
p["Gain %"] = "#{(100.0 * (0 - position[:balance]) / position[:cost]).round(1)}%"
|
92
|
+
end
|
93
|
+
pos << p
|
94
|
+
end
|
95
|
+
|
96
|
+
puts Hirb::Helpers::Table.render(pos, :fields => ["Symbol", "Last Price", "Change", "Day's Gain", "Shares", "Cost Basis", "Market Value", "Gain", "Gain %"])
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
common_transaction_options
|
101
|
+
desc 'Add buy transaction'
|
102
|
+
def buy(options={})
|
103
|
+
transaction = transaction_from_options(options)
|
104
|
+
end
|
105
|
+
|
106
|
+
common_transaction_options
|
107
|
+
desc 'Add sell transaction'
|
108
|
+
def sell(options={})
|
109
|
+
options[:quantity] = 0 - options[:quantity]
|
110
|
+
transaction = transaction_from_options(options)
|
111
|
+
end
|
112
|
+
|
113
|
+
desc 'Create Portfolio'
|
114
|
+
def create(name)
|
115
|
+
Portfolio.create(:name => name, :created_at => DateTime.now)
|
116
|
+
end
|
117
|
+
|
118
|
+
desc 'Add to watchlist'
|
119
|
+
def watch(symbol)
|
120
|
+
# Validate symbol
|
121
|
+
quote = StockFolio::Web.quote(symbol)
|
122
|
+
if nil != quote
|
123
|
+
quote.each do |q|
|
124
|
+
WatchList.create(
|
125
|
+
:symbol => "#{q["e"]}:#{q["t"]}",
|
126
|
+
:created_at => DateTime.now
|
127
|
+
)
|
128
|
+
puts "Added #{q["t"]}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
desc 'Show Watchlist'
|
134
|
+
def watchlist
|
135
|
+
items = WatchList.all(:order => [:symbol.asc])
|
136
|
+
symbols = []
|
137
|
+
items.each do |item|
|
138
|
+
symbols << item.symbol
|
139
|
+
end
|
140
|
+
quote = StockFolio::Web.quote(symbols.join(","))
|
141
|
+
print_quotes(quote)
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
def transaction_from_options(options={})
|
146
|
+
# Do we have this portfolio?!?
|
147
|
+
portfolios = Portfolio.all(:name => options[:portfolio])
|
148
|
+
if portfolios.size == 0
|
149
|
+
puts "Portfolio #{options[:portfolio]} does not exist"
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
portfolio = portfolios[0]
|
154
|
+
puts "Portfolio #{portfolio.id} - #{portfolio.name}"
|
155
|
+
|
156
|
+
# Validate the symbol
|
157
|
+
quote = StockFolio::Web.quote(options[:symbol])
|
158
|
+
if nil == quote
|
159
|
+
puts "Symbol #{options[:symbol]} not found"
|
160
|
+
return
|
161
|
+
end
|
162
|
+
|
163
|
+
transaction = Transaction.new(
|
164
|
+
:symbol => options[:symbol],
|
165
|
+
:quantity => options[:quantity],
|
166
|
+
:price => options[:price],
|
167
|
+
|
168
|
+
|
169
|
+
:fee => 0,
|
170
|
+
:portfolio => portfolio,
|
171
|
+
:executed_at => DateTime.now,
|
172
|
+
:created_at => DateTime.now
|
173
|
+
)
|
174
|
+
transaction.fee = options[:fee] if options[:fee]
|
175
|
+
transaction.executed_at = DateTime.parse(options[:date]) if options[:date]
|
176
|
+
|
177
|
+
|
178
|
+
transaction.save
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
def print_quotes(quote)
|
183
|
+
if nil != quote
|
184
|
+
# Beautify those
|
185
|
+
quote.each do |q|
|
186
|
+
q["Symbol"] = q["t"]
|
187
|
+
q["Last Price"] = "$#{q["l"]}"
|
188
|
+
q["Change"] = "#{q["c"]} (#{q["cp"]}%)"
|
189
|
+
end
|
190
|
+
puts Hirb::Helpers::Table.render(quote, fields: ["Symbol", "Last Price", "Change"])
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Transaction
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
property :id, Serial
|
5
|
+
property :symbol, String
|
6
|
+
property :quantity, Integer
|
7
|
+
property :price, Float
|
8
|
+
property :executed_at, DateTime
|
9
|
+
property :fee, Float
|
10
|
+
property :created_at, DateTime
|
11
|
+
|
12
|
+
belongs_to :portfolio
|
13
|
+
|
14
|
+
def order_name
|
15
|
+
if quantity > 0
|
16
|
+
return "BUY "
|
17
|
+
else
|
18
|
+
return "SELL"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def real_fee
|
23
|
+
if fee == nil
|
24
|
+
return 0
|
25
|
+
else
|
26
|
+
return fee
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
class StockFolio::Web
|
5
|
+
def self.quote(symbol)
|
6
|
+
url = "http://finance.google.com/finance/info?client=ig&q=#{symbol}"
|
7
|
+
|
8
|
+
resp = Net::HTTP.get_response(URI.parse(url))
|
9
|
+
data = resp.body
|
10
|
+
|
11
|
+
if data.empty?
|
12
|
+
return nil
|
13
|
+
end
|
14
|
+
JSON.parse(data.slice(3, data.length).strip)
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stockfolio
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jerome Poichet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: boson
|
16
|
+
requirement: &2165393860 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2165393860
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hirb
|
27
|
+
requirement: &2165393380 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2165393380
|
36
|
+
description:
|
37
|
+
email: poitch@gmail.com
|
38
|
+
executables:
|
39
|
+
- stockfolio
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- lib/stockfolio/portfolio.rb
|
44
|
+
- lib/stockfolio/runner.rb
|
45
|
+
- lib/stockfolio/transaction.rb
|
46
|
+
- lib/stockfolio/version.rb
|
47
|
+
- lib/stockfolio/watchlist.rb
|
48
|
+
- lib/stockfolio/web.rb
|
49
|
+
- lib/stockfolio.rb
|
50
|
+
- bin/stockfolio
|
51
|
+
homepage: http://github.com/poitch/stockfolio
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.8.10
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Track stock portfolio from the command line
|
76
|
+
test_files: []
|