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 ADDED
@@ -0,0 +1,3 @@
1
+ rticker is a command line-based stock ticker application capable of
2
+ retrieving and displaying information about stocks, futures, currency
3
+ pairs, and option contracts.
data/bin/rticker ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rticker/application'
4
+
5
+ application = RTicker::Application.new(ARGV)
6
+ application.run
data/data/all.ticker ADDED
@@ -0,0 +1,12 @@
1
+ ;; Show all ticker entries
2
+ ;; An entry starting with an @ sign represents an include directive.
3
+
4
+ @equities.ticker
5
+ --
6
+ @options.ticker
7
+ --
8
+ @indexes.ticker
9
+ --
10
+ @commodities.ticker
11
+ --
12
+ @currencies.ticker
@@ -0,0 +1,7 @@
1
+ ; Commodities are preceded by a pound sign.
2
+ *#CL.NYM,Oil
3
+ #KC.NYB,Coffee
4
+ #GC.CMX,Gold
5
+ #SI.CMX,Silver
6
+ #SB.NYB,Sugar
7
+ *#YW.CBT,Wheat
@@ -0,0 +1,7 @@
1
+ ; Currency pairs must be preceded by a dollar sign
2
+
3
+ *$EURUSD,EUR/USD
4
+ $GBPUSD,GBP/USD
5
+ $USDCHF,USD/CHF
6
+ $USDJPY,USD/JPY
7
+ $USDAUD,USD/AUD
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ ; An option contract must be preceded with an exclamation mark
2
+ !MT120121C00035000,MT call $35 Jan '12
3
+
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+
2
+ module RTicker
3
+
4
+ ##############################
5
+ # This class is simply used to represent a separator when displaying
6
+ # entries to the user.
7
+ class Separator
8
+ end
9
+
10
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'rticker/equity'
2
+ require 'rticker/future'
3
+ require 'rticker/option'
4
+ require 'rticker/currency'
5
+ require 'rticker/separator'
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
+