rticker 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
+