options-lib 0.9.2

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.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