rticker 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -0
- data/bin/rticker +6 -0
- data/data/all.ticker +12 -0
- data/data/commodities.ticker +7 -0
- data/data/currencies.ticker +7 -0
- data/data/equities.ticker +13 -0
- data/data/indexes.ticker +14 -0
- data/data/options.ticker +3 -0
- data/doc/images/Screen shot 2011-09-16 at 11.02.29 AM.png +0 -0
- data/lib/rticker/application.rb +91 -0
- data/lib/rticker/currency.rb +46 -0
- data/lib/rticker/entry.rb +36 -0
- data/lib/rticker/equity.rb +78 -0
- data/lib/rticker/future.rb +128 -0
- data/lib/rticker/net.rb +30 -0
- data/lib/rticker/option.rb +64 -0
- data/lib/rticker/options.rb +70 -0
- data/lib/rticker/parser.rb +105 -0
- data/lib/rticker/printer.rb +110 -0
- data/lib/rticker/separator.rb +10 -0
- data/lib/rticker/tput.rb +15 -0
- data/lib/rticker/types.rb +5 -0
- data/rticker.gemspec +15 -0
- metadata +94 -0
data/README
ADDED
data/bin/rticker
ADDED
data/data/all.ticker
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
;; An entry preceded with an asterisk will be displayed in bold.
|
2
|
+
|
3
|
+
AAPL
|
4
|
+
|
5
|
+
;; If you own google, list the amount of shares you own and the average
|
6
|
+
;; cost of those shares with the format COUNT@PRICE. Use a negative
|
7
|
+
;; count for short positions.
|
8
|
+
|
9
|
+
;; I wish I bought 100 shares of GOOG at $300!
|
10
|
+
GOOG
|
11
|
+
MSFT
|
12
|
+
*MT
|
13
|
+
*SLX
|
data/data/indexes.ticker
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
; Any entry starting with a caret (^) will be fetched from Yahoo.
|
2
|
+
; Otherwise the entry is fetched from Google, which is closer to
|
3
|
+
; realtime.
|
4
|
+
|
5
|
+
; A description of the ticker symbol can be supplied after a comma.
|
6
|
+
|
7
|
+
*.DJI,DJIA
|
8
|
+
.INX,S&P 500
|
9
|
+
.FTSE,FTSE.100
|
10
|
+
.HSI,HangSeng
|
11
|
+
SHA:000001,Shanghai
|
12
|
+
.N225,Nikkei 225
|
13
|
+
^^AXJO,S&P/ASX 200
|
14
|
+
^^VIX,Volatility Index
|
data/data/options.ticker
ADDED
Binary file
|
@@ -0,0 +1,91 @@
|
|
1
|
+
|
2
|
+
require 'rticker/options'
|
3
|
+
require 'rticker/parser'
|
4
|
+
require 'rticker/printer'
|
5
|
+
require 'rticker/tput'
|
6
|
+
|
7
|
+
module RTicker
|
8
|
+
|
9
|
+
DEFAULT_ENTRY_FILE = "#{ENV['HOME']}/.rticker"
|
10
|
+
|
11
|
+
class Application
|
12
|
+
|
13
|
+
def initialize (args)
|
14
|
+
# Parse recognizable options from command line. Strips away recognizable
|
15
|
+
# options from ARGV.
|
16
|
+
@options = Options.new(args)
|
17
|
+
@text_entries = []
|
18
|
+
@entries = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def run ()
|
22
|
+
# text_entries is an array of ticker entries in raw text format.
|
23
|
+
# Any left over arguments should be ticker entries.
|
24
|
+
@text_entries = @options.rest
|
25
|
+
|
26
|
+
@options.input_files.each do |file|
|
27
|
+
# For each file passed via the CLI, read its entries into text_entries.
|
28
|
+
@text_entries += RTicker::read_entry_file(file)
|
29
|
+
end
|
30
|
+
|
31
|
+
if @text_entries.empty?
|
32
|
+
# If no entries were passed via CLI, then try to read from $HOME/.rticker
|
33
|
+
@text_entries += RTicker::read_entry_file(DEFAULT_ENTRY_FILE)
|
34
|
+
end
|
35
|
+
|
36
|
+
if @text_entries.empty?
|
37
|
+
# Still no entries? Then there's nothing to do.
|
38
|
+
puts "No ticker entries provided. Exiting."
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse text_entries into instances of Equity, Future, Currency, Option,
|
43
|
+
# and Separator
|
44
|
+
@entries = @text_entries.map {|entry| RTicker::parse_entry(entry)}
|
45
|
+
|
46
|
+
currency_entries = @entries.select {|e| e.instance_of? Currency }
|
47
|
+
option_entries = @entries.select {|e| e.instance_of? Option }
|
48
|
+
future_entries = @entries.select {|e| e.instance_of? Future }
|
49
|
+
google_equity_entries = @entries.select {|e| e.instance_of? Equity and e.source == :google }
|
50
|
+
yahoo_equity_entries = @entries.select {|e| e.instance_of? Equity and e.source == :yahoo }
|
51
|
+
|
52
|
+
# Don't raise an exception when user hits Ctrl-C. Just exit silently
|
53
|
+
Signal.trap("INT") do
|
54
|
+
# Show the cursor again and shutdown cleanly.
|
55
|
+
print RTicker::tput "cnorm" unless @options.once?
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set proxy settings
|
60
|
+
if @options.proxy
|
61
|
+
RTicker::Net.proxy = @options.proxy
|
62
|
+
end
|
63
|
+
|
64
|
+
# Update entries via web calls until user quits via Ctrl-C
|
65
|
+
while true
|
66
|
+
threads = []
|
67
|
+
|
68
|
+
# Update each type of entry in its own thread.
|
69
|
+
threads << Thread.new { Currency.update(currency_entries) }
|
70
|
+
threads << Thread.new { Option.update(option_entries) }
|
71
|
+
threads << Thread.new { Future.update(future_entries) }
|
72
|
+
threads << Thread.new { Equity.update(google_equity_entries, :google) }
|
73
|
+
threads << Thread.new { Equity.update(yahoo_equity_entries, :yahoo) }
|
74
|
+
|
75
|
+
# Wait for them all to finish executing
|
76
|
+
threads.each {|t| t.join}
|
77
|
+
|
78
|
+
# Show updated info to the user
|
79
|
+
RTicker::update_screen @entries, @options.no_color?, @options.once?
|
80
|
+
|
81
|
+
# If the user only wanted us to grab this info and display it once,
|
82
|
+
# then we've done our job and its time to quit.
|
83
|
+
exit if @options.once?
|
84
|
+
|
85
|
+
sleep @options.sleep
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end # class
|
90
|
+
|
91
|
+
end # module
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rticker/entry'
|
2
|
+
require 'rticker/net'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module RTicker
|
6
|
+
|
7
|
+
##############################
|
8
|
+
# Represents currency pairs (e.g. USD/EUR)
|
9
|
+
class Currency < Entry
|
10
|
+
# In the context of currency pairs, each increment in @purchase_count
|
11
|
+
# represents single units of the quote currency exchanged for the base
|
12
|
+
# currency. (The base currency is the first of the pair, the quote
|
13
|
+
# currency is the second.) @purchase_price represents the pair ratio
|
14
|
+
# when the purchase was made. For instance, a @purchase_count of 1000
|
15
|
+
# and @purchase_price of 1.412 for the currency pair 'EUR/USD' means that
|
16
|
+
# $1000 USD was swapped for 708.21 euros. A *negative* @purchase_count
|
17
|
+
# of -1000 would mean that 1000 euros were swapped for $1,412 USD.
|
18
|
+
|
19
|
+
def Currency.update (entries)
|
20
|
+
# Strip out anything from symbol that is not an uppercase character
|
21
|
+
# This allows the user to freely use USD/EUR or USD.EUR, etc.
|
22
|
+
symbols = entries.map { |e| e.symbol.tr("^A-Z", "") }
|
23
|
+
uri_param = symbols.map{|x| x+"=X"}.join(",")
|
24
|
+
uri = "http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=sl1d1t1ba&e=.csv" % CGI::escape(uri_param)
|
25
|
+
response = RTicker::Net.get_response(uri) rescue return
|
26
|
+
return if response =~ /"N\/A"/ # Yahoo sometimes returns bogus info.
|
27
|
+
results = response.split("\n")
|
28
|
+
entries.zip(results) do |entry, result|
|
29
|
+
# Yahoo uses A CSV format.
|
30
|
+
fields = result.split(",")
|
31
|
+
price = fields[1]
|
32
|
+
last_date = fields[2]
|
33
|
+
return if Date.strptime(last_date, '"%m/%d/%Y"') != Date.today
|
34
|
+
if price.to_f != entry.curr_value and not entry.curr_value.nil?
|
35
|
+
# The price has changed
|
36
|
+
entry.last_changed = Time.now()
|
37
|
+
end
|
38
|
+
entry.curr_value = price.to_f
|
39
|
+
# Currencies aren't regulated. So no need to calculate the
|
40
|
+
# @start_value, because there is no market open. Currencies trade
|
41
|
+
# 24/7
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end # class Currency
|
45
|
+
|
46
|
+
end # module
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module RTicker
|
3
|
+
|
4
|
+
##############################
|
5
|
+
# Parent class to represent all types of ticker entries to watch. The
|
6
|
+
# classes that subclass from Entry are: Equity, Currency, Future, and
|
7
|
+
# Option.
|
8
|
+
class Entry
|
9
|
+
|
10
|
+
# @symbol represents the ticker symbol for an entry (e.g., AAPL, MSFT,
|
11
|
+
# etc.) @description is an optional string that can give a more
|
12
|
+
# user-friendly description of a symbol. If a description is not
|
13
|
+
# provided, the symbol itself is displayed to the user. If the user
|
14
|
+
# provides information on having purchased an entry (for example AAPL
|
15
|
+
# stock), the user may specify how many via @purchase_count (in this case
|
16
|
+
# how many shares), and for how much via @purchase_price (in this case
|
17
|
+
# how much paid per share). A user can specify a negative
|
18
|
+
# @purchase_count to represent a SHORT position. @bold specifies if
|
19
|
+
# this entry should stand apart from other entries when displayed to the
|
20
|
+
# user.
|
21
|
+
attr_accessor :symbol, :description, :purchase_count, :purchase_price
|
22
|
+
attr_accessor :bold, :curr_value, :last_changed
|
23
|
+
|
24
|
+
alias bold? bold
|
25
|
+
|
26
|
+
def initialize (symbol, description=nil, purchase_count=nil, purchase_price=nil, bold=false)
|
27
|
+
@symbol = symbol
|
28
|
+
@description = description
|
29
|
+
@purchase_count = purchase_count
|
30
|
+
@purchase_price = purchase_price
|
31
|
+
@bold = bold
|
32
|
+
@last_changed = nil
|
33
|
+
end
|
34
|
+
end # class Entry
|
35
|
+
|
36
|
+
end # module
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rticker/entry'
|
2
|
+
require 'rticker/net'
|
3
|
+
require 'cgi'
|
4
|
+
require 'rational'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module RTicker
|
8
|
+
|
9
|
+
##############################
|
10
|
+
# Represents stocks/equities (e.g. AAPL, MSFT)
|
11
|
+
class Equity < Entry
|
12
|
+
# Equities can be retrieved via either Google Finance or Yahoo Finance.
|
13
|
+
# @source should be set to one of either :google (the default) or :yahoo.
|
14
|
+
# @start_value represents the value of the equity at market open.
|
15
|
+
|
16
|
+
attr_accessor :start_value, :source
|
17
|
+
|
18
|
+
def initialize (symbol, description=nil, purchase_count=nil, purchase_price=nil, bold=false)
|
19
|
+
super(symbol, description, purchase_count, purchase_price, bold)
|
20
|
+
@start_value=nil
|
21
|
+
@source=:google
|
22
|
+
end
|
23
|
+
|
24
|
+
def Equity.update (entries, source=:google)
|
25
|
+
if source == :google
|
26
|
+
Equity.update_google(entries)
|
27
|
+
elsif source == :yahoo
|
28
|
+
Equity.update_yahoo(entries)
|
29
|
+
else
|
30
|
+
raise ArgumentError, "Unexpected symbol: #{source.to_s}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def Equity.update_google (entries)
|
35
|
+
symbols = entries.map { |e| e.symbol }
|
36
|
+
uri = "http://www.google.com/finance/info?client=ig&q=%s" % CGI::escape(symbols.join(","))
|
37
|
+
response = RTicker::Net.get_response(uri) rescue return
|
38
|
+
return if response =~ /illegal/ # Google didn't like our request.
|
39
|
+
results = response.split("{")[1..-1]
|
40
|
+
return if results.nil?
|
41
|
+
entries.zip(results) do |entry, result|
|
42
|
+
# Fish out the info we want. Could use a JSON library, but that's
|
43
|
+
# one more gem that would be required of the user to install. Opted
|
44
|
+
# instead to just use some ugly regexs
|
45
|
+
price = /,"l" : "([^"]*)"/.match(result)[1].tr(",", "") rescue return
|
46
|
+
change = /,"c" : "([^"]*)"/.match(result)[1].tr(",", "") rescue return
|
47
|
+
if price.to_f != entry.curr_value and not entry.curr_value.nil?
|
48
|
+
# The price has changed
|
49
|
+
entry.last_changed = Time.now()
|
50
|
+
end
|
51
|
+
entry.curr_value = price.to_f
|
52
|
+
entry.start_value = entry.curr_value - change.to_f
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def Equity.update_yahoo (entries)
|
57
|
+
symbols = entries.map { |e| e.symbol }
|
58
|
+
uri = "http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=l1c1d1va2xj1b4j4dyekjm3m4rr5p5p6s7" % CGI::escape(symbols.join(","))
|
59
|
+
response = RTicker::Net.get_response(uri) rescue return
|
60
|
+
results = response.split("\n")
|
61
|
+
entries.zip(results) do |entry, result|
|
62
|
+
# Yahoo uses A CSV format.
|
63
|
+
fields = result.split(",")
|
64
|
+
price = fields[0]
|
65
|
+
change = fields[1]
|
66
|
+
last_date = fields[2]
|
67
|
+
return if Date.strptime(last_date, '"%m/%d/%Y"') != Date.today
|
68
|
+
if price.to_f != entry.curr_value and not entry.curr_value.nil?
|
69
|
+
# The price has changed
|
70
|
+
entry.last_changed = Time.now()
|
71
|
+
end
|
72
|
+
entry.curr_value = price.to_f
|
73
|
+
entry.start_value = entry.curr_value - change.to_f
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end # class Equity
|
77
|
+
|
78
|
+
end # module
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'rticker/entry'
|
2
|
+
require 'rticker/net'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module RTicker
|
6
|
+
|
7
|
+
##############################
|
8
|
+
# Represents futures contracts (e.g., oil, gold, corn, wheat)
|
9
|
+
class Future < Entry
|
10
|
+
# Futures contracts expire on a given month and year. Let's track that
|
11
|
+
# information to display to the user. Might as well show the @market
|
12
|
+
# that the contract is traded on, too. For instance oil can be traded via
|
13
|
+
# NYMEX or ICE (InterContinental Exchange). Also, because we won't know
|
14
|
+
# ahead of time what the nearest expiration for a contract will be, we'll
|
15
|
+
# have to figure this out for each contract with web calls. Once we know
|
16
|
+
# the expiration, we'll calculate the real symbol for the contract and
|
17
|
+
# store it in @real_symbol.
|
18
|
+
attr_accessor :start_value, :exp_month, :exp_year, :market
|
19
|
+
attr_accessor :real_symbol
|
20
|
+
|
21
|
+
# Commodities have strange codes for months
|
22
|
+
COMMODITY_MONTHS = %w[F G H J K M N Q U V X Z]
|
23
|
+
|
24
|
+
def initialize (symbol, description=nil, purchase_count=nil, purchase_price=nil, bold=false)
|
25
|
+
super(symbol, description, purchase_count, purchase_price, bold)
|
26
|
+
@start_value=nil
|
27
|
+
@exp_month=nil
|
28
|
+
@exp_year=nil
|
29
|
+
@market=nil
|
30
|
+
@real_symbol=nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def Future.update (entries)
|
34
|
+
# Futures are special. The first run requires us to find the spot
|
35
|
+
# contract for the commodity in question. The spot contract is simply
|
36
|
+
# the closest expiring contract that is still open for trades.
|
37
|
+
# Determining the spot contract is not always clear, and so we must
|
38
|
+
# brute force to find the answer. When we find the spot contract, we
|
39
|
+
# set the entry's real_symbol attribute to the actual spot contract's
|
40
|
+
# symbol. Got it?
|
41
|
+
|
42
|
+
# Since variables are shared in block closures, a simple for loop
|
43
|
+
# won't do. We must create a new Proc to eliminate sharing of the
|
44
|
+
# entry variable.
|
45
|
+
go = Proc.new do |entry|
|
46
|
+
Thread.new { entry.determine_spot_contract }
|
47
|
+
end
|
48
|
+
|
49
|
+
if entries.any? {|e| e.real_symbol.nil?}
|
50
|
+
#puts "Please wait... determining spot contracts"
|
51
|
+
threads = []
|
52
|
+
for entry in entries
|
53
|
+
if not entry.real_symbol
|
54
|
+
threads << go.call(entry)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
threads.each {|t| t.join}
|
58
|
+
end
|
59
|
+
|
60
|
+
# All spot contracts have been found. Now let's update our entries
|
61
|
+
# with the latest price information. This is what we came here for!
|
62
|
+
symbols = entries.map { |e| e.real_symbol }
|
63
|
+
uri = "http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=l1c1d1va2xj1b4j4dyekjm3m4rr5p5p6s7" % CGI.escape(symbols.join(","))
|
64
|
+
response = RTicker::Net.get_response(uri) rescue return
|
65
|
+
results = response.split("\n")
|
66
|
+
entries.zip(results) do |_entry, result|
|
67
|
+
# Yahoo uses A CSV format.
|
68
|
+
fields = result.split(",")
|
69
|
+
return if fields[4] == '""' # This is a sign yahoo is giving us bad info
|
70
|
+
price = fields[0]
|
71
|
+
change = fields[1]
|
72
|
+
last_date = fields[2]
|
73
|
+
return if Date.strptime(last_date, '"%m/%d/%Y"') != Date.today
|
74
|
+
if price.to_f != _entry.curr_value and not _entry.curr_value.nil?
|
75
|
+
# The price has changed
|
76
|
+
_entry.last_changed = Time.now()
|
77
|
+
end
|
78
|
+
_entry.curr_value = price.to_f
|
79
|
+
_entry.start_value = _entry.curr_value - change.to_f
|
80
|
+
end
|
81
|
+
end # def
|
82
|
+
|
83
|
+
def determine_spot_contract
|
84
|
+
# This is as nasty as it gets. Keep trying contracts, month after
|
85
|
+
# month, until we find a valid one. Stop after 9 failed attempts.
|
86
|
+
attempt_count = 9 # Maximum number of attempts to try
|
87
|
+
|
88
|
+
symbol, exchange = @symbol.split(".")
|
89
|
+
@market = exchange
|
90
|
+
curr_month = curr_year = nil
|
91
|
+
|
92
|
+
while attempt_count > 0
|
93
|
+
if curr_month.nil?
|
94
|
+
# By default start looking at next month. Also, let's use a zero
|
95
|
+
# based index into months. Jan == 0, Dec == 11
|
96
|
+
curr_month = Time.now().month % 12
|
97
|
+
curr_year = Time.now().year
|
98
|
+
else
|
99
|
+
curr_month = (curr_month + 1) % 12
|
100
|
+
end
|
101
|
+
curr_year += 1 if curr_month == 0 # We've rolled into next year
|
102
|
+
month_symbol = Future::COMMODITY_MONTHS[curr_month]
|
103
|
+
year_symbol = curr_year % 100 # Only want last two digits of year.
|
104
|
+
real_symbol_attempt = "#{symbol}#{month_symbol}#{year_symbol}.#{exchange}"
|
105
|
+
uri = "http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=l1c1va2xj1b4j4dyekjm3m4rr5p5p6s7" % CGI::escape(real_symbol_attempt)
|
106
|
+
response = RTicker::Net.get_response(uri) rescue return
|
107
|
+
|
108
|
+
# This contract is only valid if the response doesn't start with
|
109
|
+
# 0.00. A commodity is never worth nothing!
|
110
|
+
unless response =~ /^0.00/
|
111
|
+
@real_symbol = real_symbol_attempt
|
112
|
+
@exp_month = curr_month+1 # Convert from 0-based back to 1-based
|
113
|
+
@exp_year = curr_year
|
114
|
+
break # Get out of this loop!
|
115
|
+
end
|
116
|
+
attempt_count -= 1
|
117
|
+
end # while
|
118
|
+
|
119
|
+
if attempt_count == 0 and @real_symbol.nil?
|
120
|
+
# Can't determine month for this contract. Set the real_symbol to
|
121
|
+
# symbol and hope for the best.
|
122
|
+
@real_symbol = @symbol
|
123
|
+
end
|
124
|
+
end # def
|
125
|
+
|
126
|
+
end # class Future
|
127
|
+
|
128
|
+
end # module
|
data/lib/rticker/net.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module RTicker
|
5
|
+
class Net
|
6
|
+
|
7
|
+
@@proxy_host = nil
|
8
|
+
@@proxy_port = nil
|
9
|
+
|
10
|
+
def self.proxy ()
|
11
|
+
return nil if @@proxy_host.nil?
|
12
|
+
port = @@proxy_port || 80
|
13
|
+
return "#{@@proxy_host}:#{port}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.proxy= (proxy)
|
17
|
+
host, port = proxy.split(":")
|
18
|
+
@@proxy_host = host
|
19
|
+
@@proxy_port = port || 80
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.get_response (url)
|
23
|
+
begin
|
24
|
+
return ::Net::HTTP::Proxy(@@proxy_host, @@proxy_port).get(URI.parse url)
|
25
|
+
rescue Timeout::Error => e
|
26
|
+
return ""
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rticker/entry'
|
2
|
+
require 'rticker/net'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module RTicker
|
6
|
+
|
7
|
+
##############################
|
8
|
+
# Represents option contracts which are derivatives of either equities or
|
9
|
+
# futures.
|
10
|
+
class Option < Entry
|
11
|
+
# Because option contracts are not traded as much as their underlying,
|
12
|
+
# the bid/ask spread can be pretty high and there might be quite a bit of
|
13
|
+
# time since a trade has occurred for a given contract. Therefore it is
|
14
|
+
# sometimes more meaningful to represent the value of an option by
|
15
|
+
# showing both the current bid and ask prices rather than the last trade
|
16
|
+
# price.
|
17
|
+
attr_accessor :bid, :ask
|
18
|
+
|
19
|
+
def initialize (symbol, description=nil, purchase_count=nil, purchase_price=nil, bold=false)
|
20
|
+
super(symbol, description, purchase_count, purchase_price, bold)
|
21
|
+
@bid = @ask = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def Option.update (entries)
|
25
|
+
go = Proc.new do |entry|
|
26
|
+
Thread.new { Option.run_update entry }
|
27
|
+
end
|
28
|
+
|
29
|
+
threads = []
|
30
|
+
for entry in entries
|
31
|
+
# Can only grab one option contract at a time, so request each one in
|
32
|
+
# a separate thread.
|
33
|
+
threads << go.call(entry)
|
34
|
+
end
|
35
|
+
threads.each {|t| t.join}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def Option.run_update (entry)
|
41
|
+
uri = "http://finance.yahoo.com/q?s=%s" % CGI::escape(entry.symbol)
|
42
|
+
response = RTicker::Net.get_response(uri) rescue Thread.current.exit
|
43
|
+
# Rake through the HTML and find the bits we want.
|
44
|
+
# This can be a bit messy.
|
45
|
+
begin
|
46
|
+
bid = /id="yfs_b00_[^"]*">([^<]*)</.match(response)[1]
|
47
|
+
ask = /id="yfs_a00_[^"]*">([^<]*)</.match(response)[1]
|
48
|
+
rescue Exception => e
|
49
|
+
# These results aren't available from about 9am to 9:30am.
|
50
|
+
# Yahoo's results are often 20-30 minutes behind.
|
51
|
+
Thread.current.exit
|
52
|
+
end
|
53
|
+
if (bid.to_f != entry.bid or ask.to_f != entry.ask) and not entry.bid.nil?
|
54
|
+
# The price has changed
|
55
|
+
entry.last_changed = Time.now()
|
56
|
+
end
|
57
|
+
entry.bid = bid.to_f
|
58
|
+
entry.ask = ask.to_f
|
59
|
+
# The value is the mean average of the bid/ask spread
|
60
|
+
entry.curr_value = (entry.bid + entry.ask) / 2
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end # module
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'optparse' # Use OptionParser to parse command line args
|
2
|
+
|
3
|
+
module RTicker
|
4
|
+
|
5
|
+
class Options
|
6
|
+
|
7
|
+
attr_reader :once, :no_color, :input_files
|
8
|
+
attr_reader :sleep, :rest, :proxy
|
9
|
+
|
10
|
+
def initialize (args)
|
11
|
+
# Defaults
|
12
|
+
@once = false # Only run once then quit
|
13
|
+
@no_color = false # Don't use color when showing results to user
|
14
|
+
@input_files = [] # Input files provided by user
|
15
|
+
@proxy = nil # "host:port" proxy
|
16
|
+
@sleep = 2 # how long to delay between web requests
|
17
|
+
@rest = [] # The rest of the command line arguments
|
18
|
+
parse!(args)
|
19
|
+
end
|
20
|
+
|
21
|
+
alias no_color? no_color
|
22
|
+
alias once? once
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parse! (args)
|
27
|
+
# Use OptionParser to parse command line arguments
|
28
|
+
OptionParser.new do |opts|
|
29
|
+
opts.banner = "Usage: rticker [-onh] [-d SECS] [-p host:port] [-f FILE] ... [SYMBOL[,DESC[,COUNT@PRICE]]] ..."
|
30
|
+
|
31
|
+
opts.on("-o", "--once", "Only display ticker results once then quit") do
|
32
|
+
@once = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-n", "--no-color", "Do not use colors when displaying results") do
|
36
|
+
@no_color = true
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-f", "--file FILE", "Specify a file that lists ticker entries, one per line") do |file|
|
40
|
+
@input_files << file
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-d", "--delay SECS", "How long to delay in SECS between each update") do |secs|
|
44
|
+
@sleep = secs.to_f
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("-p", "--proxy HOST:PORT", "Host and port of HTTP proxy to use.") do |proxy|
|
48
|
+
@proxy = proxy
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on("-h", "--help", "Display this screen") do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parse options and pop recognized options out of ARGV.
|
57
|
+
# If a parse error occurs, print help and exit
|
58
|
+
begin
|
59
|
+
opts.parse!(args)
|
60
|
+
rescue OptionParser::ParseError => e
|
61
|
+
STDERR.puts e.message, "\n", opts
|
62
|
+
exit(-1)
|
63
|
+
end
|
64
|
+
@rest = args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class
|
69
|
+
|
70
|
+
end # module
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'rticker/types'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module RTicker
|
5
|
+
|
6
|
+
##############################
|
7
|
+
# Read raw text entries, line by line, from file. Return result as an
|
8
|
+
# array of text entries. File "-" represents STDIN.
|
9
|
+
def RTicker.read_entry_file (file, context=nil)
|
10
|
+
context ||= Dir.pwd
|
11
|
+
|
12
|
+
# If this is a relative path, make it absolute according to context
|
13
|
+
if file != "-" and file[0,1] != "/"
|
14
|
+
file = [context, file].join("/")
|
15
|
+
end
|
16
|
+
|
17
|
+
# If we're not dealing with a readable source then get out of here
|
18
|
+
return [] if file != "-" and not File.readable? file
|
19
|
+
|
20
|
+
if File.symlink? file
|
21
|
+
# Follow symlinks so that we can honor relative include directives
|
22
|
+
return read_entry_file(Pathname.new(file).realpath.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
entries = []
|
26
|
+
source = if file == "-" then $stdin else File.open(file) end
|
27
|
+
source.each do |line|
|
28
|
+
line.chomp!
|
29
|
+
case line
|
30
|
+
when /^;/, /^\s*$/
|
31
|
+
# If this line starts with a semicolon (comment) or only contains
|
32
|
+
# whitespace, ignore it.
|
33
|
+
next
|
34
|
+
when /^@.+/
|
35
|
+
# If this line starts with an @ sign, then this is an 'include'
|
36
|
+
# directive. Ie, this entry is simply including another entry file.
|
37
|
+
# Eg: @otherfile.txt
|
38
|
+
# This will include another file called otherfile.txt
|
39
|
+
include_file = line[1..-1]
|
40
|
+
context = File.dirname(file) unless file == "-"
|
41
|
+
entries += read_entry_file(include_file, context)
|
42
|
+
else
|
43
|
+
entries << line
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
entries
|
48
|
+
end # read_entry_file
|
49
|
+
|
50
|
+
##############################
|
51
|
+
# Parse a raw text entry into an instance of Equity, Future, Currency,
|
52
|
+
# Option, or Separator.
|
53
|
+
def RTicker.parse_entry (entry)
|
54
|
+
|
55
|
+
# An entry starting with a dash represents a Separator
|
56
|
+
return Separator.new if entry[0,1] == "-"
|
57
|
+
|
58
|
+
# An entry beginning with an asterisk represents a bold entry.
|
59
|
+
bold = false
|
60
|
+
if entry[0,1] == "*"
|
61
|
+
bold = true
|
62
|
+
entry = entry[1..-1] # Shift off asterisk
|
63
|
+
end
|
64
|
+
|
65
|
+
## Here is a breakdown of the format:
|
66
|
+
## [sign]symbol[,desc[,purchase_count@purchase_price]]
|
67
|
+
## Where sign can be a "#" (Future), "!" (Option), "^" (Yahoo Equity),
|
68
|
+
## or "$" (Currency). Anything else represents a Google Equity.
|
69
|
+
pattern = /^([#!^$])?([^,]+)(?:,([^,]*)(?:,(-?[0-9]+)@([0-9]+\.?[0-9]*))?)?/
|
70
|
+
match = pattern.match entry
|
71
|
+
|
72
|
+
sign = match[1]
|
73
|
+
symbol = match[2]
|
74
|
+
# Because description is optional, let's make it clear with a nil that no
|
75
|
+
# description was provided.
|
76
|
+
description = match[3] == "" ? nil : match[3]
|
77
|
+
purchase_count = match[4]
|
78
|
+
purchase_price = match[5]
|
79
|
+
|
80
|
+
args = [symbol, description, purchase_count.to_f, purchase_price.to_f, bold]
|
81
|
+
case sign
|
82
|
+
when "#"
|
83
|
+
# An entry starting with a hash is a Future. A useful mnemonic is to
|
84
|
+
# think of the "pound" sign and think of lbs of a commodity.
|
85
|
+
return Future.new(*args)
|
86
|
+
when "!"
|
87
|
+
# An entry starting with an exclamation mark is an option contract.
|
88
|
+
return Option.new(*args)
|
89
|
+
when "^"
|
90
|
+
# An entry starting with a caret represents an equity to be fetched
|
91
|
+
# from Yahoo Finance.
|
92
|
+
e = Equity.new(*args)
|
93
|
+
e.source = :yahoo
|
94
|
+
return e
|
95
|
+
when "$"
|
96
|
+
# An entry starting with a dollar sign represents a currency pair.
|
97
|
+
return Currency.new(*args)
|
98
|
+
else
|
99
|
+
# Everthing else is an equity to be fetched from Google Finance (the
|
100
|
+
# default).
|
101
|
+
return Equity.new(*args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end # module
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'rticker/tput'
|
2
|
+
require 'rticker/types'
|
3
|
+
|
4
|
+
module RTicker
|
5
|
+
|
6
|
+
##############################
|
7
|
+
# Update the screen with the latest information.
|
8
|
+
def RTicker.update_screen (entries, no_color=false, once=false)
|
9
|
+
# An interesting side-effect of using unless here is that if no_color is
|
10
|
+
# true, then all of these variables are still created, but set to nil.
|
11
|
+
# So setting no_color effectively turns all of the following variables
|
12
|
+
# into empty strings, turning off terminal manipulation.
|
13
|
+
hide_cursor = tput "civis" unless no_color
|
14
|
+
show_cursor = tput "cnorm" unless no_color
|
15
|
+
bold = tput "bold" unless no_color
|
16
|
+
unbold = tput "sgr0" unless no_color
|
17
|
+
default = tput "sgr0" unless no_color
|
18
|
+
red = tput "setaf 1" unless no_color
|
19
|
+
green = tput "setaf 2" unless no_color
|
20
|
+
blue = tput "setaf 4" unless no_color
|
21
|
+
|
22
|
+
if not once
|
23
|
+
print %x[clear]
|
24
|
+
print "#{hide_cursor}"
|
25
|
+
end
|
26
|
+
print "#{default}"
|
27
|
+
|
28
|
+
# Calculate the longest symbol name or description
|
29
|
+
max_length = 0
|
30
|
+
for entry in entries.select {|e| not e.is_a? Separator}
|
31
|
+
output = entry.description || entry.symbol
|
32
|
+
max_length = [output.size, max_length].max
|
33
|
+
end
|
34
|
+
max_length += 2 # Give a little extra breathing room
|
35
|
+
|
36
|
+
for entry in entries
|
37
|
+
if entry.is_a? Separator
|
38
|
+
print "___________________________________________\n"
|
39
|
+
next # Skip to the next entry
|
40
|
+
end
|
41
|
+
|
42
|
+
# Prefer a description over a raw symbol name. Use max_length to left
|
43
|
+
# align the output with extra padding on the right. This lines
|
44
|
+
# everything up nice and neat.
|
45
|
+
output = "%-#{max_length}s" % (entry.description || entry.symbol)
|
46
|
+
output = "#{bold}#{output}#{unbold}" if entry.bold?
|
47
|
+
|
48
|
+
# If we're still waiting for valid responses from yahoo or google, then
|
49
|
+
# just let the user know that they need to wait.
|
50
|
+
curr_value = entry.curr_value || "please wait..."
|
51
|
+
|
52
|
+
if entry.is_a? Option
|
53
|
+
curr_value = "#{entry.bid}/#{entry.ask}" if entry.bid
|
54
|
+
end
|
55
|
+
|
56
|
+
# Does this entry have a start_value? If so, let's calculate the
|
57
|
+
# change in percent.
|
58
|
+
change_string = nil
|
59
|
+
if entry.respond_to? :start_value and not entry.start_value.nil?
|
60
|
+
change = entry.curr_value - entry.start_value
|
61
|
+
change_percent = (change / entry.start_value) * 100
|
62
|
+
color = (change >= 0 ? "#{green}" : "#{red}")
|
63
|
+
change_string = " (#{color}%+.2f %%%0.2f#{default})" % [change, change_percent]
|
64
|
+
end
|
65
|
+
|
66
|
+
# If this entry has purchase info, let's calculate profit/loss
|
67
|
+
profit_string = nil
|
68
|
+
if entry.purchase_count != 0 and not entry.curr_value.nil?
|
69
|
+
count = entry.purchase_count
|
70
|
+
price = entry.purchase_price
|
71
|
+
# Options are purchased in multiples of 100 of the contract price
|
72
|
+
count *= 100 if entry.is_a? Option
|
73
|
+
# There is also a price multiplier for futures, but they are
|
74
|
+
# completely different for each contract. So the user will simply
|
75
|
+
# need to enter the correct multiplier when configuring the
|
76
|
+
# purchase_count in the ticker entry. For instance, a contract for
|
77
|
+
# CL, crude light sweet oil, has a multiplier of 1000. Ie, one
|
78
|
+
# contract represents 1000 barrels of oil. So if a contract is
|
79
|
+
# bought, the user should enter a purchase_count of 1000. If the
|
80
|
+
# contract is sold (a short position), the purchase_count should be
|
81
|
+
# -1000.
|
82
|
+
profit_loss = count * entry.curr_value - count * price
|
83
|
+
profit_loss_percent = profit_loss / (count*price) * 100
|
84
|
+
color = (profit_loss >= 0 ? "#{green}" : "#{red}")
|
85
|
+
profit_string = " = #{color}$%.2f %%%0.2f#{default}" % [profit_loss, profit_loss_percent]
|
86
|
+
end
|
87
|
+
|
88
|
+
case entry
|
89
|
+
when Equity
|
90
|
+
print "#{output} #{curr_value}#{change_string}#{profit_string}"
|
91
|
+
when Future
|
92
|
+
print "#{output} #{curr_value}#{change_string}#{profit_string}"
|
93
|
+
when Option
|
94
|
+
print "#{output} #{curr_value}#{profit_string}"
|
95
|
+
when Currency
|
96
|
+
print "#{output} #{curr_value}#{profit_string}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Let the user know with a blue asterisk if this entry has changed
|
100
|
+
# within the past 5 minutes.
|
101
|
+
if entry.last_changed and (Time.now() - entry.last_changed) <= 5*60
|
102
|
+
print " #{blue}*#{default}"
|
103
|
+
end
|
104
|
+
|
105
|
+
print "\n"
|
106
|
+
end # for
|
107
|
+
|
108
|
+
end # def
|
109
|
+
|
110
|
+
end
|
data/lib/rticker/tput.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module RTicker
|
2
|
+
|
3
|
+
##############################
|
4
|
+
# tput is a command line utility used for sending special terminal codes.
|
5
|
+
# This method is memoized to cut down on execution time.
|
6
|
+
$__tput_cache = {}
|
7
|
+
def RTicker.tput (args)
|
8
|
+
if $__tput_cache.has_key? args
|
9
|
+
$__tput_cache[args]
|
10
|
+
else
|
11
|
+
$__tput_cache[args] = %x[tput #{args}]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/rticker.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = "rticker"
|
4
|
+
s.summary = "Command line-based stock ticker application"
|
5
|
+
s.description = File.read(File.join(File.dirname(__FILE__), 'README'))
|
6
|
+
s.version = "1.1.1"
|
7
|
+
s.author = "Alfred J. Fazio"
|
8
|
+
s.email = "alfred.fazio@gmail.com"
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.required_ruby_version = '>=1.8.7'
|
11
|
+
s.files = Dir['**/**']
|
12
|
+
s.files -= Dir["pkg/**"]
|
13
|
+
s.executables = ['rticker']
|
14
|
+
s.has_rdoc = false
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rticker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 17
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 1.1.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Alfred J. Fazio
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-12-13 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: |-
|
23
|
+
rticker is a command line-based stock ticker application capable of
|
24
|
+
retrieving and displaying information about stocks, futures, currency
|
25
|
+
pairs, and option contracts.
|
26
|
+
email: alfred.fazio@gmail.com
|
27
|
+
executables:
|
28
|
+
- rticker
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- bin/rticker
|
35
|
+
- data/all.ticker
|
36
|
+
- data/commodities.ticker
|
37
|
+
- data/currencies.ticker
|
38
|
+
- data/equities.ticker
|
39
|
+
- data/indexes.ticker
|
40
|
+
- data/options.ticker
|
41
|
+
- doc/images/Screen shot 2011-09-16 at 11.02.29 AM.png
|
42
|
+
- lib/rticker/application.rb
|
43
|
+
- lib/rticker/currency.rb
|
44
|
+
- lib/rticker/entry.rb
|
45
|
+
- lib/rticker/equity.rb
|
46
|
+
- lib/rticker/future.rb
|
47
|
+
- lib/rticker/net.rb
|
48
|
+
- lib/rticker/option.rb
|
49
|
+
- lib/rticker/options.rb
|
50
|
+
- lib/rticker/parser.rb
|
51
|
+
- lib/rticker/printer.rb
|
52
|
+
- lib/rticker/separator.rb
|
53
|
+
- lib/rticker/tput.rb
|
54
|
+
- lib/rticker/types.rb
|
55
|
+
- README
|
56
|
+
- rticker.gemspec
|
57
|
+
has_rdoc: true
|
58
|
+
homepage:
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 57
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 8
|
75
|
+
- 7
|
76
|
+
version: 1.8.7
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.6.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Command line-based stock ticker application
|
93
|
+
test_files: []
|
94
|
+
|