options-lib 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,9 @@
1
+ # Options-Lib
2
+
3
+ A set of classes for dealing with options. It includes a crawler for Yahoo!Finance. The crawler has an internal
4
+ thread that can be started to periodically update the option quotes.
5
+
6
+ ## Usage
7
+
8
+ TODO
9
+
data/bin/show_quotes ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'options-lib'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'options-lib'
8
+ end
9
+
10
+ if ARGV.length != 5
11
+ puts "format: show_quotes <stock> <call_or_put> <strike> <expiration> <reload_period>"
12
+ puts "example: show_quotes AAPL C 345 2013-01-18 5"
13
+ exit
14
+ end
15
+
16
+ stock = ARGV[0]
17
+ is_call = ARGV[1].casecmp("C") == 0
18
+ strike = ARGV[2].to_f
19
+ expiration = ARGV[3]
20
+ reload_period = ARGV[4].to_i
21
+
22
+ y = YahooCrawler.new(stock, expiration)
23
+
24
+ y.auto_reload(reload_period) do
25
+
26
+ options = is_call ? y.call_options : y.put_options
27
+ quote = options[strike]
28
+ stock_price = y.curr_stock_price
29
+
30
+ puts "Stock @ #{stock_price} Options: #{quote.bid} | #{quote.ask} Spread: #{quote.spread}"
31
+
32
+ end
33
+
34
+ y.join_reload_thread
35
+
@@ -0,0 +1,36 @@
1
+ class Float
2
+
3
+ # Supress decimal part if it is zero
4
+ # 40.0 becomes "40"
5
+ # Take care of float imprecision
6
+ # 1.1-0.9 # >> 0.20000000000000007
7
+ # (1.1-0.9).prettify # >> "0.2"
8
+ def prettify
9
+ num = "%.12g" % self
10
+ num.sub!($1, '') if num =~ /\..*?(0+)$/
11
+ # might be like 2. at this point
12
+ num = num[0..-2] if num[-1] == '.'
13
+ num
14
+ end
15
+ end
16
+
17
+ class Array
18
+
19
+ # Break the array into smaller arrays (chuncks)
20
+ # If the lenght of the array is not divisible by the number of chunks (pieces)
21
+ # then first chunk will accomodate the extra item and be the larger chunk
22
+ # Ex: [1,2,3,4].chunck # => [[1,2], [3,4]]
23
+ def chunk(pieces=2)
24
+ len = self.length;
25
+ mid = (len / pieces)
26
+ chunks = []
27
+ start = 0
28
+ 1.upto(pieces) do |i|
29
+ last = start + mid
30
+ last = last - 1 unless len % pieces >= i
31
+ chunks << self[start..last] || []
32
+ start = last + 1
33
+ end
34
+ chunks
35
+ end
36
+ end
@@ -0,0 +1,62 @@
1
+ require 'date'
2
+ require_relative 'helpers'
3
+
4
+ class Option
5
+
6
+ CALL = 'CALL'
7
+ PUT = 'PUT'
8
+
9
+ attr_reader :stock, :exp, :type, :strike, :symbol, :internal_symbol
10
+
11
+ def initialize(type, stock, strike, exp, symbol = nil)
12
+ raise "Invalid type: #{type}" if type != CALL and type != PUT
13
+
14
+ @type, @stock, @strike = type, stock, strike.to_f
15
+
16
+ if exp =~ /(\d{4})[\-\/](\d{1,2})[\-\/](\d{1,2})/
17
+ @exp = Date.new($1.to_i, $2.to_i, $3.to_i)
18
+ else
19
+ raise "Cannot parse expiration date: #{exp}"
20
+ end
21
+
22
+ s = "#{stock}_#{type == CALL ? 'C' : 'P'}#{@strike.prettify}_#{@exp.day}"
23
+ s << Date::MONTHNAMES[@exp.month][0,3].upcase
24
+ s << @exp.year.to_s
25
+ @internal_symbol = s
26
+
27
+ if not symbol
28
+ @symbol = @internal_symbol
29
+ else
30
+ @symbol = symbol
31
+ end
32
+ end
33
+
34
+ def is_call?
35
+ @type == CALL
36
+ end
37
+
38
+ def is_put?
39
+ @type == PUT
40
+ end
41
+
42
+ # Business days (= minus Sat and Sun) until expiration
43
+ def days_to_exp
44
+ total = 0
45
+ curr_day = Date.today
46
+ while curr_day <= exp
47
+ total += 1 if curr_day.wday != 0 and curr_day.wday != 6
48
+ curr_day = curr_day.next
49
+ end
50
+ total
51
+ end
52
+
53
+ def to_s
54
+ @internal_symbol
55
+ end
56
+
57
+ def inspect
58
+ @internal_symbol
59
+ end
60
+ end
61
+
62
+
@@ -0,0 +1,29 @@
1
+ require_relative 'option'
2
+
3
+ class OptionQuote
4
+
5
+ attr_reader :option, :bid, :ask
6
+
7
+ def initialize(option, args)
8
+ @option = option
9
+ @bid = args[:bid] || nil
10
+ @ask = args[:ask] || nil
11
+ end
12
+
13
+ def spread
14
+ if @bid and @ask
15
+ (@ask - @bid).prettify.to_f
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def to_s
22
+ "#{option.to_s}: #{bid.inspect} / #{ask.inspect}"
23
+ end
24
+
25
+ def inspect
26
+ "#{option.inspect}: bid => #{bid.inspect}, ask => #{ask.inspect}"
27
+ end
28
+
29
+ end
@@ -0,0 +1,169 @@
1
+ require 'thread'
2
+ require 'Mechanize'
3
+
4
+ require_relative 'option'
5
+ require_relative 'option_quote'
6
+ require_relative 'helpers'
7
+
8
+ # A Yahoo!Finance crawler implemented using Mechanize to parse HTML and extract
9
+ # options quotes for given stock and expiration date.
10
+ class YahooCrawler
11
+
12
+ def initialize(stock, exp)
13
+ @mech = Mechanize.new
14
+ @stock, @exp = stock, exp
15
+ @url = "http://finance.yahoo.com/q/op?s=#{stock}&m=#{exp[0,7]}"
16
+ @stock_curr_price = nil
17
+ @call_options = Hash.new
18
+ @put_options = Hash.new
19
+ @call_strikes = Array.new
20
+ @put_strikes = Array.new
21
+ @lock = Mutex.new
22
+ @t_lock = Mutex.new
23
+ @thread = nil
24
+ end
25
+
26
+ def auto_reload(period = 60)
27
+ if not @thread.nil?
28
+ stop
29
+ end
30
+
31
+ @thread = Thread.new do
32
+ loop do
33
+ begin
34
+ fetch
35
+ yield # callback
36
+ rescue => e
37
+ puts "Error fetching data: #{e.message}"
38
+ end
39
+ sleep period
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ def stop
46
+ if not @thread.nil?
47
+ @thread.kill
48
+ @thread = nil
49
+ end
50
+ end
51
+
52
+ def join_reload_thread
53
+ @thread.join if @thread
54
+ end
55
+
56
+ def fetch
57
+
58
+ c_options, p_options = Hash.new, Hash.new
59
+ c_strikes, p_strikes = Array.new, Array.new
60
+ stock_price = nil
61
+
62
+ @t_lock.synchronize { # don't step into each other in case someone calls fetch
63
+
64
+ page = @mech.get(@url).body
65
+
66
+ curr_price = page.scan(/\: \<.+?\<\/big\>/)
67
+ lines = parse_data(curr_price[0])
68
+ stock_price = lines[0].to_f
69
+
70
+ calls = page.scan(/Strike\<.+Put Options/)
71
+ puts = page.scan(/Put Options\<.+Highlighted/)
72
+
73
+ lines = parse_data(calls[0])
74
+ lines = lines.chunk(lines.length / 8)
75
+
76
+ lines.each do |array|
77
+ quote = parse_quote(array, Option::CALL)
78
+ c_options[quote.option.strike] = quote
79
+ end
80
+
81
+ lines = parse_data(puts[0])
82
+ lines = lines.chunk(lines.length / 8)
83
+
84
+ lines.each do |array|
85
+ quote = parse_quote(array, Option::PUT)
86
+ p_options[quote.option.strike] = quote
87
+ end
88
+
89
+ c_options.keys.sort.each { |key| c_strikes << key }
90
+
91
+ p_options.keys.sort.each { |key| p_strikes << key }
92
+
93
+ }
94
+
95
+ @lock.synchronize {
96
+ @call_options, @put_options = c_options, p_options
97
+ @call_strikes, @put_strikes = c_strikes, p_strikes
98
+ @curr_stock_price = stock_price
99
+ }
100
+
101
+ end
102
+
103
+ def get_option_quote(type, strike)
104
+ if type == Option::CALL
105
+ get_call_option_quote(strike)
106
+ else
107
+ get_put_option_quote(strike)
108
+ end
109
+ end
110
+
111
+ def get_call_option_quote(strike)
112
+ call_options[strike]
113
+ end
114
+
115
+ def get_put_option_quote(strike)
116
+ put_options[strike]
117
+ end
118
+
119
+ def call_strikes
120
+ @lock.synchronize { @call_strikes }
121
+ end
122
+
123
+ def put_strikes
124
+ @lock.synchronize { @put_strikes }
125
+ end
126
+
127
+ def call_options
128
+ @lock.synchronize { @call_options }
129
+ end
130
+
131
+ def put_options
132
+ @lock.synchronize { @put_options }
133
+ end
134
+
135
+ def curr_stock_price
136
+ @lock.synchronize { @curr_stock_price }
137
+ end
138
+
139
+ def show_calls
140
+ call_strikes.each { |key| puts call_options[key] }
141
+ end
142
+
143
+ def show_puts
144
+ put_strikes.each { |key| puts put_options[key] }
145
+ end
146
+
147
+ private
148
+
149
+ def parse_quote(line, type)
150
+ strike = line[0].to_f
151
+ symbol = line[1]
152
+ bid = f line[4]
153
+ ask = f line[5]
154
+
155
+ o = Option.new(type, @stock, strike, @exp, symbol)
156
+ OptionQuote.new(o, :bid => bid, :ask => ask)
157
+ end
158
+
159
+ # Get all values inside tags
160
+ # Ex: <h1>234</h1> will return "234"
161
+ def parse_data(data)
162
+ data.scan(/\>[0-9\.A-Z\,\/]+\</).collect { |i| i.gsub(/[\<\>]/, "") }
163
+ end
164
+
165
+ def f(data)
166
+ data == 'N/A' ? nil : data.to_f
167
+ end
168
+
169
+ end
@@ -0,0 +1,6 @@
1
+ # require all files from options-lib
2
+ require 'options-lib/helpers'
3
+ require 'options-lib/option'
4
+ require 'options-lib/option_quote'
5
+ require 'options-lib/yahoo_crawler'
6
+
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'options-lib'
3
+ s.version = '0.9.2'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = 'Options Library'
6
+ s.description = 'A set of classes for dealing with options. It includes a crawler for Yahoo!Finance.'
7
+
8
+ s.author = 'Sergio Oliveira Jr.'
9
+ s.email = 'sergio.oliveira.jr@gmail.com'
10
+ s.homepage = 'https://github.com/saoj/options-lib'
11
+
12
+ # These dependencies are only for people who work on this gem
13
+ s.add_development_dependency 'rspec'
14
+
15
+ # Runtime dependencies
16
+ s.add_runtime_dependency 'mechanize'
17
+
18
+ # The list of files to be contained in the gem
19
+ s.files = `git ls-files`.split("\n") - [".gitignore"]
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+
23
+ # Supress the warning about no rubyforge project
24
+ s.rubyforge_project = 'nowarning'
25
+
26
+ end
@@ -0,0 +1,46 @@
1
+ # require all files from lib
2
+ Dir["./lib/*.rb"].each {|file| require file }
3
+
4
+ describe Float do
5
+
6
+ it 'can format its value in a pretty way' do
7
+
8
+ # if it has a decimal part, print it
9
+ f = 50.1
10
+ f.prettify.should == "50.1"
11
+
12
+ # if decimal part is 0, do not print it
13
+ f = 50.0
14
+ f.prettify.should == "50"
15
+
16
+ # take care of ruby float imprecision
17
+ f = 1.1 - 0.9
18
+ f.should_not == 0.2 # weird but true !!!
19
+ f.prettify.should == "0.2" # much better!
20
+ end
21
+ end
22
+
23
+ describe Array do
24
+
25
+ it 'can be broken into chunks of smaller arrays' do
26
+
27
+ # the first chunk is always the bigger in case the number of
28
+ # elements are not divisible by pieces
29
+
30
+ a = [1,2,3,4]
31
+ a.chunk.should == [[1,2],[3,4]]
32
+ a.chunk(3).should == [[1,2],[3],[4]]
33
+ a.chunk(4).should == [[1],[2],[3],[4]]
34
+
35
+ a << 5 << 6
36
+ a.chunk.should == [[1,2,3],[4,5,6]]
37
+ a.chunk(3).should == [[1,2],[3,4],[5,6]]
38
+
39
+ a << 7
40
+ a.chunk.should == [[1,2,3,4],[5,6,7]]
41
+ a.chunk(3).should == [[1,2,3],[4,5],[6,7]]
42
+
43
+ end
44
+
45
+ end
46
+
@@ -0,0 +1,23 @@
1
+ # require all files from lib
2
+ Dir["./lib/*.rb"].each {|file| require file }
3
+
4
+ describe OptionQuote do
5
+
6
+ it 'has option, bid and ask prices' do
7
+
8
+ o = Option.new(Option::CALL, 'AAPL', 250, '2010-02-14')
9
+ q = OptionQuote.new(o, :bid => 1.2, :ask => 1.4)
10
+ q.option.should == o
11
+ q.bid.should == 1.2
12
+ q.ask.should == 1.4
13
+
14
+ end
15
+
16
+ it 'should have an spread' do
17
+ o = Option.new(Option::CALL, 'AAPL', 250, '2010-02-14')
18
+ q = OptionQuote.new(o, :bid => 1.2, :ask => 1.4)
19
+ q.spread.prettify.should == "0.2"
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,63 @@
1
+ # require all files from lib
2
+ Dir["./lib/*.rb"].each {|file| require file }
3
+
4
+ describe Option do
5
+
6
+ it 'can be a call or a put' do
7
+ o = Option.new(Option::CALL, 'AAPL', 250, '2010-02-14')
8
+ o.type.should == Option::CALL
9
+ o.type.should == 'CALL'
10
+
11
+ o = Option.new(Option::PUT, 'AAPL', 250, '2010-02-14')
12
+ o.type.should == Option::PUT
13
+ o.type.should == 'PUT'
14
+ end
15
+
16
+ it 'has stock and strike price' do
17
+ o = Option.new(Option::CALL, 'AAPL', 250, '2010-02-14')
18
+ o.stock.should == 'AAPL'
19
+ o.strike.should == 250
20
+
21
+ end
22
+
23
+ it 'converts expiration to a nice date object' do
24
+ o = Option.new(Option::PUT, 'AAPL', 250, '2010-08-14')
25
+ o.exp.day.should == 14
26
+ o.exp.month.should == 8
27
+ o.exp.year.should == 2010
28
+
29
+ o = Option.new(Option::CALL, 'AAPL', 250, '2011/09/15')
30
+ o.exp.day.should == 15
31
+ o.exp.month.should == 9
32
+ o.exp.year.should == 2011
33
+ end
34
+
35
+ it 'throws an exception if date is bad' do
36
+ lambda {Option.new(Option::PUT, 'AAPL', 250, '10-08-14')}.should raise_error
37
+ lambda {Option.new(Option::PUT, 'AAPL', 250, '10-58-14')}.should raise_error
38
+ lambda {Option.new(Option::PUT, 'AAPL', 250, '2010-122-14')}.should raise_error
39
+ end
40
+
41
+ it 'has an internal symbol representation implied from stock, strike and experiration' do
42
+ o = Option.new(Option::PUT, 'AAPL', 250, '2010-08-14')
43
+ o.internal_symbol.should == 'AAPL_P250_14AUG2010'
44
+ end
45
+
46
+ it 'makes the symbol equals to the internal symbol if symbol not provided' do
47
+ o = Option.new(Option::PUT, 'AAPL', 250, '2010-08-14')
48
+ o.internal_symbol.should == o.symbol
49
+ end
50
+
51
+ it 'can have a symbol' do
52
+ o = Option.new(Option::PUT, 'AAPL', 250, '2010-08-14', 'MYSYMBOL')
53
+ o.symbol.should == 'MYSYMBOL'
54
+ o.internal_symbol.should == 'AAPL_P250_14AUG2010'
55
+ end
56
+
57
+ it 'can show to number of business day until expiration' do
58
+ d = Date.today + 10
59
+ o = Option.new(Option::PUT, 'AAPL', 250, d.to_s)
60
+ o.days_to_exp.should < 10
61
+ end
62
+
63
+ end
@@ -0,0 +1,59 @@
1
+ # require all files from lib
2
+ Dir["./lib/*.rb"].each {|file| require file }
3
+
4
+ describe YahooCrawler do
5
+
6
+ y = nil
7
+
8
+ before(:all) do
9
+ y = YahooCrawler.new('AAPL', '2013-01-18')
10
+ y.fetch
11
+ end
12
+
13
+ it 'should be able to fetch options quotes from Yahoo!Finance' do
14
+
15
+ y.call_options.length.should > 0
16
+ y.put_options.length.should > 0
17
+
18
+ end
19
+
20
+ it 'should be able to get current price of stock' do
21
+
22
+ y.curr_stock_price.should > 0
23
+
24
+ end
25
+
26
+ it 'should be able to return an array of strike prices' do
27
+
28
+ y.call_strikes.length.should > 0
29
+ y.put_strikes.length.should > 0
30
+
31
+ end
32
+
33
+ it 'should be able to give an option quote by strike price' do
34
+
35
+ # get a good strike price by rounding the current stock price
36
+ good_price = ((y.curr_stock_price / 100).to_i * 100).to_f
37
+
38
+ call_quote = y.call_options[good_price]
39
+ call_quote.option.internal_symbol.should == "AAPL_C#{good_price.to_i}_18JAN2013"
40
+ call_quote.bid.should > 0 if not call_quote.bid.nil?
41
+ call_quote.ask.should > 0 if not call_quote.ask.nil?
42
+
43
+ put_quote = y.put_options[good_price]
44
+ put_quote.option.internal_symbol.should == "AAPL_P#{good_price.to_i}_18JAN2013"
45
+ put_quote.bid.should > 0 if not put_quote.bid.nil?
46
+ put_quote.ask.should > 0 if not put_quote.ask.nil?
47
+
48
+ end
49
+
50
+ it 'should be able to update the quotes in a background thread' do
51
+
52
+ y.auto_reload(1)
53
+ y.call_options.length.should > 0
54
+ y.put_options.length.should > 0
55
+
56
+ end
57
+
58
+
59
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: options-lib
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.9.2
6
+ platform: ruby
7
+ authors:
8
+ - Sergio Oliveira Jr.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-02-24 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: mechanize
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ description: A set of classes for dealing with options. It includes a crawler for Yahoo!Finance.
39
+ email: sergio.oliveira.jr@gmail.com
40
+ executables:
41
+ - show_quotes
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - README.markdown
48
+ - bin/show_quotes
49
+ - lib/options-lib.rb
50
+ - lib/options-lib/helpers.rb
51
+ - lib/options-lib/option.rb
52
+ - lib/options-lib/option_quote.rb
53
+ - lib/options-lib/yahoo_crawler.rb
54
+ - options-lib.gemspec
55
+ - spec/helpers_spec.rb
56
+ - spec/option_quote_spec.rb
57
+ - spec/option_spec.rb
58
+ - spec/yahoo_crawler_spec.rb
59
+ has_rdoc: true
60
+ homepage: https://github.com/saoj/options-lib
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project: nowarning
83
+ rubygems_version: 1.5.2
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Options Library
87
+ test_files:
88
+ - spec/helpers_spec.rb
89
+ - spec/option_quote_spec.rb
90
+ - spec/option_spec.rb
91
+ - spec/yahoo_crawler_spec.rb