rticker 1.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/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
|
+
|