oh 1.0.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.
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2010-12-02
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,14 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/oh
7
+ lib/oh.rb
8
+ lib/ohbjects.rb
9
+ lib/ohbjects/account.rb
10
+ lib/ohbjects/options.rb
11
+ lib/ohbjects/quote.rb
12
+ test/test_oh.rb
13
+ test/test_ohbjects.rb
14
+ test/test_options.rb
@@ -0,0 +1,67 @@
1
+ = oh
2
+
3
+ * http://github.com/aasmith/oh
4
+
5
+ == Description
6
+
7
+ An API for OptionsHouse (http://optionshouse.com).
8
+
9
+ Currently provides a mechanism for pulling stock and option quotes.
10
+
11
+ == Synopsis
12
+
13
+ # The basic oh API. Calls to the API return parsed Nokogiri documents.
14
+ require 'oh'
15
+
16
+ # An object wrapper around the basic api. Marshals above responses into
17
+ # handy Ruby objects, defined in lib/ohbjects.
18
+ require 'ohbjects'
19
+ Ohbjects.activate
20
+
21
+ # You defined these vars somewhere else, right?
22
+ o = Oh.new(username, password)
23
+
24
+ # Use the virtual account so we don't accidentally spend all your
25
+ # hard-earned dollars on Frozen Pork Belly futures.
26
+ account = o.accounts.detect { |a| a.virtual? }
27
+ o.account_id = account.id
28
+
29
+ # Bask is the glory of knowing the latest price for the
30
+ # iPath Dow Jones-AIG Coffee Total Return Sub-Index ETN.
31
+ p o.quote("JO")
32
+
33
+ # Do something like this every 120 seconds or so, otherwise your
34
+ # token will expire.
35
+ o.keep_alive
36
+
37
+ == Requirements
38
+
39
+ * Nokogiri
40
+ * An OptionsHouse account
41
+
42
+ == Install
43
+
44
+ sudo gem install oh
45
+
46
+ == License
47
+
48
+ Copyright (c) 2010 Andrew A. Smith
49
+
50
+ Permission is hereby granted, free of charge, to any person obtaining
51
+ a copy of this software and associated documentation files (the
52
+ 'Software'), to deal in the Software without restriction, including
53
+ without limitation the rights to use, copy, modify, merge, publish,
54
+ distribute, sublicense, and/or sell copies of the Software, and to
55
+ permit persons to whom the Software is furnished to do so, subject to
56
+ the following conditions:
57
+
58
+ The above copyright notice and this permission notice shall be
59
+ included in all copies or substantial portions of the Software.
60
+
61
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
62
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
63
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
64
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
65
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
66
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
67
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/oh.rb'
4
+
5
+ Hoe.spec 'oh' do
6
+ developer('Andrew A. Smith', 'andy@tinnedfruit.org')
7
+
8
+ extra_deps << %w(nokogiri)
9
+
10
+ self.readme_file = "README.rdoc"
11
+ end
12
+
data/bin/oh ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'oh'
4
+ require 'ohbjects'
5
+ Ohbjects.activate
6
+
7
+ username, *symbols = ARGV
8
+
9
+ abort "Usage: #{$0} [username] [symbol ...]" unless username and !symbols.empty?
10
+
11
+ begin
12
+ print "Password: "
13
+ system "stty -echo"
14
+ password = $stdin.gets.chomp
15
+ ensure
16
+ system "stty echo"
17
+ puts
18
+ end
19
+
20
+ o = Oh.new(username, password)
21
+
22
+ account = o.accounts.detect { |a| a.virtual? }
23
+
24
+ if account
25
+ puts "Using account #{account.name}"
26
+ else
27
+ abort "No virtual accounts found for #{username.inspect}"
28
+ end
29
+
30
+ o.account_id = account.id
31
+
32
+ Thread.abort_on_exception = true
33
+
34
+ auth_last_sent_at = Time.now
35
+
36
+ begin
37
+ puts "Started polling at #{st=Time.now}"
38
+
39
+ t = Thread.new do
40
+ loop do
41
+ symbols.each do |symbol|
42
+ p o.quote(symbol)
43
+ end
44
+
45
+ if Time.now - auth_last_sent_at > 120
46
+ o.keep_alive
47
+ auth_last_sent_at = Time.now
48
+ end
49
+
50
+ sleep 1
51
+ end
52
+ end
53
+
54
+ t.join
55
+ ensure
56
+ abort "Terminated at #{et=Time.now} after #{et-st} secs."
57
+ end
@@ -0,0 +1,202 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ require 'stringio'
5
+ require 'zlib'
6
+
7
+ require 'rubygems'
8
+ require 'nokogiri'
9
+
10
+ class Oh
11
+ VERSION = '1.0.1'
12
+
13
+ HEADERS = {
14
+ "Host" => "www.optionshouse.com",
15
+ "User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 (.NET CLR 3.5.30729)",
16
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
17
+ "Accept-Language" => "en-us,en;q=0.5",
18
+ "Accept-Encoding" => "gzip,deflate",
19
+ "Accept-Charset" => "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
20
+ "Connection" => "keep-alive",
21
+ "Content-Type" => "text/xml; charset=UTF-8",
22
+ "Referer" => "https://www.optionshouse.com/tool/login/",
23
+ "Cookie" => "blackbird={pos:1,size:0,load:null,info:true,debug:true,warn:true,error:true,profile:true}",
24
+ "Pragma" => "no-cache",
25
+ "Cache-Control" => "no-cache",
26
+ }
27
+
28
+ attr_accessor :username, :password
29
+ attr_writer :account_id
30
+
31
+ def initialize(username, password)
32
+ self.username = username
33
+ self.password = password
34
+ end
35
+
36
+ def account_id
37
+ @account_id or raise "Account not set; call Oh#accounts() to get a list, then set using Oh#account_id=(id)."
38
+ end
39
+
40
+ def token
41
+ @token ||= login
42
+ end
43
+
44
+ def login
45
+ response = request_without_callbacks(message("auth.login",
46
+ :userName => username,
47
+ :password => password,
48
+ :organization => "OPHOUSE,KERSHNER",
49
+ :authToken => nil,
50
+ :validationText => ""))
51
+
52
+ access = response.search("//access").text
53
+ raise AuthError, "Access was #{access}" unless access == "granted"
54
+
55
+ ready = response.search("//requiresAccountCreation").text == "false"
56
+ raise AccountError, "Account is not active" unless ready
57
+
58
+ token = response.search("//authToken").text
59
+ raise AuthError, "No auth token was returned" if token.strip.empty? or token == "null"
60
+
61
+ token
62
+ end
63
+
64
+ def accounts
65
+ request(message_with_token("account.info"))
66
+ end
67
+
68
+ def quote(symbol)
69
+ request(messages(quote_messages(symbol)))
70
+ end
71
+
72
+ def option_chain(symbol)
73
+ request(chain_message(symbol))
74
+ end
75
+
76
+ def quote_with_chain(symbol)
77
+ request(messages(quote_messages(symbol), chain_message(symbol)))
78
+ end
79
+
80
+ # TODO: send this every 120 seconds?
81
+ def keep_alive
82
+ request(message_with_account("auth.keepAlive"))
83
+ end
84
+
85
+ def quote_messages(symbol)
86
+ [
87
+ message_with_account("view.quote",
88
+ :symbol => symbol,
89
+ :description => true,
90
+ :fundamentals => true),
91
+ message_with_token("echo", :symbol => symbol)
92
+ ]
93
+ end
94
+
95
+ def chain_message(symbol)
96
+ message_with_account("view.chain",
97
+ :symbol => symbol,
98
+ :greeks => true,
99
+ :weeklies => true,
100
+ :quarterlies => true,
101
+ :quotesAfter => 0,
102
+ :ntm => 10, # near the money
103
+ :bs => true) # black-scholes?
104
+ end
105
+
106
+ def message_with_token(action, data = {})
107
+ message(action, {:authToken => token}.merge(data))
108
+ end
109
+
110
+ def message_with_account(action, data = {})
111
+ message_with_token(action, {:account => account_id}.merge(data))
112
+ end
113
+
114
+ def message(action, data = {})
115
+ chunks = []
116
+
117
+ chunks << "<EZMessage action='#{action}'>"
118
+ chunks << "<data>"
119
+
120
+ data.each do |key, value|
121
+ chunks << "<#{key}>#{value || "null"}</#{key}>"
122
+ end
123
+
124
+ chunks << "</data>"
125
+ chunks << "</EZMessage>"
126
+
127
+ chunks.join
128
+ end
129
+
130
+ def messages(*messages)
131
+ "<EZList>#{messages.flatten.join}</EZList>"
132
+ end
133
+
134
+ def request_without_callbacks(body)
135
+ request(body, false)
136
+ end
137
+
138
+ def request(body, with_callbacks = true)
139
+ path = "/m"
140
+
141
+ response = connection.post(path, body, HEADERS)
142
+ data = response.body
143
+
144
+ out = case response["content-encoding"]
145
+ when /gzip/ then Zlib::GzipReader.new(StringIO.new(data)).read
146
+ when /deflate/ then Zlib::Inflate.inflate(data)
147
+ else data
148
+ end
149
+
150
+ if $DEBUG
151
+ puts "Sent:"
152
+ puts body
153
+ puts "-" * 80
154
+ puts "Got"
155
+ p response.code
156
+ p response.message
157
+
158
+ response.each {|key, val| puts key + ' = ' + val}
159
+
160
+ puts out
161
+ puts "=" * 80
162
+ puts
163
+ end
164
+
165
+ result = begin
166
+ Nokogiri.parse(out, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
167
+ rescue
168
+ warn "Unable to parse: #{out.inspect}"
169
+ raise
170
+ end
171
+
172
+ check_connection_status(result)
173
+
174
+ with_callbacks && respond_to?(:post_process_request) ?
175
+ post_process_request(result) :
176
+ result
177
+ end
178
+
179
+ def check_connection_status(doc)
180
+ if doc.at("//errors/access[text()='denied']")
181
+ raise AuthError, "Access denied, token has expired?"
182
+ end
183
+ end
184
+
185
+ def connection
186
+ return @client if defined? @client
187
+
188
+ client = Net::HTTP.new("www.optionshouse.com", 443)
189
+ client.use_ssl = true
190
+
191
+ if ENV["SSL_PATH"] && File.exist?(ENV["SSL_PATH"])
192
+ client.ca_path = ENV["SSL_PATH"]
193
+ client.verify_mode = OpenSSL::SSL::VERIFY_PEER
194
+ end
195
+
196
+ @client = client
197
+ end
198
+
199
+ AuthError = Class.new(StandardError)
200
+ AccountError = Class.new(StandardError)
201
+
202
+ end
@@ -0,0 +1,67 @@
1
+ # Adds an object representation to Oh responses:
2
+ #
3
+ # require "ohbjects"
4
+ # Ohbjects.activate
5
+ #
6
+ # Then, any further calls to Oh methods that used to
7
+ # return an XML doc will now return objects such as
8
+ # Ohbjects::Call, Ohbjects::Put, etc.
9
+ #
10
+ module Ohbjects
11
+ REGISTRY = []
12
+
13
+ module Buildable
14
+ attr_reader :spec
15
+
16
+ def builds(css_or_xpath)
17
+ @spec = css_or_xpath
18
+ end
19
+
20
+ def build?(doc)
21
+ !doc.search(@spec).empty?
22
+ end
23
+
24
+ # Taken from active support.
25
+ def underscore(camel_cased_word)
26
+ camel_cased_word.to_s.
27
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
28
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
29
+ tr("-", "_").
30
+ downcase
31
+ end
32
+
33
+ def self.extended(extender)
34
+ REGISTRY << extender
35
+ end
36
+ end
37
+
38
+ def objectify(doc)
39
+ qualified_builders =
40
+ REGISTRY.select { |builder| builder.build?(doc) }
41
+
42
+ objects = []
43
+
44
+ qualified_builders.each do |builder|
45
+ doc.search(builder.spec).each do |fragment|
46
+ objects.push(*builder.build(fragment))
47
+ end
48
+ end
49
+
50
+ objects
51
+ end
52
+
53
+ def post_process_request(result)
54
+ objectify(result)
55
+ end
56
+
57
+ class << self
58
+ def activate
59
+ Oh.send :include, self
60
+ end
61
+ end
62
+ end
63
+
64
+ Dir[File.join(File.dirname(__FILE__), 'ohbjects', '*.rb')].each do |fn|
65
+ require fn
66
+ end
67
+
@@ -0,0 +1,29 @@
1
+ module Ohbjects
2
+ class AccountBuilder
3
+ extend Buildable
4
+
5
+ # Not building anything real for now.
6
+ builds "//data/account[isVirtual='true']"
7
+
8
+ class << self
9
+ def build(fragment)
10
+ account = Account.new
11
+
12
+ account.id = fragment.at("accountId").text
13
+ account.name = fragment.at("accountName").text
14
+ account.virtual = fragment.at("isVirtual").text =~ /true/
15
+ account.display_id = fragment.at("account").text
16
+
17
+ account
18
+ end
19
+ end
20
+ end
21
+
22
+ class Account
23
+ attr_accessor :id, :display_id, :name, :virtual
24
+
25
+ def virtual?
26
+ virtual
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,78 @@
1
+ module Ohbjects
2
+ class OptionBuilder
3
+ extend Buildable
4
+
5
+ builds "optionQuote"
6
+
7
+ METHOD_LOOKUP = {
8
+ :optionRoot => :root,
9
+ :strikePrice => :strike,
10
+ :expDate => :expires,
11
+ :bidSize => :bids,
12
+ :askSize => :asks
13
+ }
14
+
15
+ class << self
16
+ def build(fragment)
17
+ put, call = Put.new, Call.new
18
+
19
+ fragment.attributes.each do |key, attr|
20
+ match = key.match(/^(call|put)?(.*)$/)
21
+
22
+ option_type, method = match.captures
23
+
24
+ method = METHOD_LOOKUP[method.to_sym] || underscore(method)
25
+ setter = "#{method}="
26
+
27
+ value = attr.value
28
+
29
+ if option_type
30
+ option = option_type == "call" ? call : put
31
+ option.send(setter, value) if option.respond_to?(setter)
32
+ else
33
+ [put, call].each do |option|
34
+ option.send(setter, value) if option.respond_to?(setter)
35
+ end
36
+ end
37
+ end
38
+
39
+ [put, call]
40
+ end
41
+ end
42
+ end
43
+
44
+ class Option
45
+ attr_accessor :id, :key, :strike, :root,
46
+ :bid, :ask, :change, :volume, :open_interest,
47
+ :iv, :delta, :gamma, :theta, :vega,
48
+ :bids, :asks
49
+
50
+ attr_reader :expires
51
+
52
+ def initialize
53
+ if instance_of? Option
54
+ raise ArgumentError, "An option cannot be instantiated"
55
+ end
56
+ end
57
+
58
+ def expires=(date_string)
59
+ # Intentionally explict pattern to force
60
+ # parse errors should the format change.
61
+ @expires = Date.strptime(date_string, "%b %e, %Y")
62
+ end
63
+
64
+ def call?
65
+ Call === self
66
+ end
67
+
68
+ def put?
69
+ Put === self
70
+ end
71
+ end
72
+
73
+ class Put < Option
74
+ end
75
+
76
+ class Call < Option
77
+ end
78
+ end
@@ -0,0 +1,35 @@
1
+ module Ohbjects
2
+ class QuoteBuilder
3
+ extend Buildable
4
+
5
+ builds "//data/quote"
6
+
7
+ class << self
8
+ def build(fragment)
9
+ quote = Quote.new
10
+
11
+ fragment.attributes.each do |key, attr|
12
+ method = "#{underscore(key)}="
13
+
14
+ next unless quote.respond_to?(method)
15
+
16
+ quote.send(method, attr.value)
17
+ end
18
+
19
+ quote.description =
20
+ fragment.attributes["shortDescription"].value
21
+
22
+ quote
23
+ end
24
+ end
25
+ end
26
+
27
+ class Quote
28
+ attr_accessor :symbol, :bid, :ask, :volume, :prev_close, :last,
29
+ :open, :high, :low, :today_close, :description
30
+ end
31
+
32
+ # TODO: today_close will be 0 until today has actually closed.
33
+ # TODO: many more attributes
34
+ # TODO: field conversion
35
+ end
@@ -0,0 +1,166 @@
1
+ require "test/unit"
2
+ require "flexmock/test_unit"
3
+ require "oh"
4
+
5
+ class TestOh < Test::Unit::TestCase
6
+ def setup
7
+ @oh = Oh.new("bob", "secret")
8
+ end
9
+
10
+ def mocked_response(data, headers = {})
11
+ response = headers.dup
12
+
13
+ class << response
14
+ attr_accessor :body
15
+ end
16
+
17
+ response.body = data
18
+ response
19
+ end
20
+
21
+ def test_request_doesnt_delegate_when_not_present
22
+ flexmock(Net::HTTP).new_instances.should_receive(
23
+ :post => mocked_response("<response></response>")
24
+ )
25
+
26
+ flexmock(@oh) do |m|
27
+ m.should_receive(:post_process_request).never
28
+ m.should_receive(:respond_to?).
29
+ with(:post_process_request).and_return(false)
30
+ end
31
+
32
+ @oh.request("<request></request>")
33
+ end
34
+
35
+ def test_request_doesnt_delegate_when_asked_not_to
36
+ flexmock(Net::HTTP).new_instances.should_receive(
37
+ :post => mocked_response("<response></response>")
38
+ )
39
+
40
+ flexmock(@oh) do |m|
41
+ m.should_receive(:post_process_request).never
42
+ m.should_receive(:respond_to?).
43
+ with(:post_process_request).and_return(true)
44
+ end
45
+
46
+ @oh.request("<request></request>", false)
47
+ end
48
+
49
+ def test_request_without_callbacks_calls_request_with_correct_args
50
+ flexmock(@oh).should_receive(:request).with("foo", false).once
51
+
52
+ @oh.request_without_callbacks("foo")
53
+ end
54
+
55
+ def test_request_delegates
56
+ flexmock(Net::HTTP).new_instances.should_receive(
57
+ :post => mocked_response("<response></response>")
58
+ )
59
+
60
+ flexmock(@oh) do |m|
61
+ m.should_receive(:post_process_request).once
62
+ m.should_receive(:respond_to?).
63
+ with(:post_process_request).and_return(true)
64
+ end
65
+
66
+ @oh.request("<request></request>")
67
+ end
68
+
69
+ def test_request_handles_gzip
70
+ io = StringIO.new
71
+
72
+ z = Zlib::GzipWriter.new(io)
73
+ z.write "<response></response>"
74
+ z.close
75
+
76
+ flexmock(Net::HTTP).new_instances.should_receive(
77
+ :post => mocked_response(
78
+ io.string,
79
+ "content-encoding" => "gzip"
80
+ ))
81
+
82
+ doc = @oh.request("<request></request>")
83
+
84
+ assert_equal "response", doc.root.name,
85
+ "should have document with a root node of response"
86
+ end
87
+
88
+ def test_request_handles_deflate
89
+ flexmock(Net::HTTP).new_instances.should_receive(
90
+ :post => mocked_response(
91
+ Zlib::Deflate.deflate("<response></response>"),
92
+ "content-encoding" => "deflate"
93
+ ))
94
+
95
+ doc = @oh.request("<request></request>")
96
+
97
+ assert_equal "response", doc.root.name,
98
+ "should have document with a root node of response"
99
+ end
100
+
101
+ def test_request_handles_plaintext
102
+ flexmock(Net::HTTP).new_instances.should_receive(
103
+ :post => mocked_response("<response></response>")
104
+ )
105
+
106
+ doc = @oh.request("<request></request>")
107
+
108
+ assert_equal "response", doc.root.name,
109
+ "should have document with a root node of response"
110
+ end
111
+
112
+ def test_request_raises_with_malformed_xml
113
+ flexmock(Net::HTTP).new_instances.should_receive(
114
+ :post => mocked_response("<invalid></xml>")
115
+ )
116
+
117
+ old_stderr = $stderr
118
+ $stderr = StringIO.new
119
+
120
+ assert_raise Nokogiri::XML::SyntaxError do
121
+ doc = @oh.request("<request></request>")
122
+ end
123
+
124
+ assert_match %r{Unable to parse}, $stderr.string
125
+
126
+ $stderr = old_stderr
127
+ end
128
+
129
+ def test_request_sends_consistent_header_and_path
130
+ m = flexmock("Net::HTTP instance")
131
+ m.should_ignore_missing
132
+ m.should_receive(:post).
133
+ with("/m", "<request></request>", Oh::HEADERS).
134
+ and_return(mocked_response("<response></response>")).
135
+ once
136
+
137
+ flexmock(Net::HTTP).should_receive(:new).once.and_return(m)
138
+
139
+ @oh.request("<request></request>")
140
+ end
141
+
142
+ def test_connection_reuses_single_client
143
+ assert_same @oh.connection, @oh.connection, "Should be cached"
144
+ end
145
+
146
+ def test_message
147
+ msg = @oh.message("test.action", :foo => nil, :bar => 2)
148
+
149
+ assert_match %r{<foo>null</foo>}, msg, "should convert nils to null"
150
+ assert_match %r{<bar>2</bar>}, msg, "should to_s all other objects"
151
+ end
152
+
153
+ def test_messages
154
+ messages = %w(<message_one> <message_two>)
155
+ nested_messages = [messages, messages]
156
+
157
+ assert_equal "<EZList>#{messages.join}</EZList>",
158
+ @oh.messages(messages)
159
+
160
+ assert_equal "<EZList>#{nested_messages.flatten.join}</EZList>",
161
+ @oh.messages(nested_messages), "should flatten nested messages"
162
+ end
163
+
164
+ def test_account_raises_when_not_set
165
+ end
166
+ end
@@ -0,0 +1,63 @@
1
+ require "test/unit"
2
+ require "flexmock/test_unit"
3
+
4
+ require "oh"
5
+ require "ohbjects"
6
+
7
+ class TestOhbjects < Test::Unit::TestCase
8
+
9
+ def test_objectify
10
+ o = Object.new
11
+ class << o
12
+ include Ohbjects
13
+ end
14
+
15
+ klass = Class.new
16
+
17
+ klass.instance_eval do
18
+ extend Ohbjects::Buildable
19
+ builds "foo"
20
+ end
21
+
22
+ flexmock(klass).should_receive(:build => "built-foo").once
23
+
24
+ Ohbjects::REGISTRY.clear
25
+ Ohbjects::REGISTRY << klass
26
+
27
+ output = o.objectify(Nokogiri("<doc><foo /></doc>"))
28
+
29
+ assert_equal %w(built-foo), output
30
+ end
31
+
32
+ def test_activate_includes_ohbjects_into_oh
33
+ flexmock(Oh).should_receive(:include).with(Ohbjects).once
34
+
35
+ Ohbjects.activate
36
+ end
37
+
38
+ def test_buildable_build?
39
+ klass = Class.new
40
+
41
+ klass.instance_eval do
42
+ extend Ohbjects::Buildable
43
+ builds "foo"
44
+ end
45
+
46
+ assert klass.build?(Nokogiri("<doc><foo></foo></doc>"))
47
+ assert !klass.build?(Nokogiri("<doc><bar></bar></doc>"))
48
+ end
49
+
50
+ def test_buildable_adds_to_registry
51
+ before = Ohbjects::REGISTRY.size
52
+
53
+ klass = Class.new
54
+
55
+ klass.instance_eval do
56
+ extend Ohbjects::Buildable
57
+ builds "foo"
58
+ end
59
+
60
+ assert_equal before + 1, Ohbjects::REGISTRY.size
61
+ assert_equal klass, Ohbjects::REGISTRY.pop
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ require "test/unit"
2
+ require "flexmock/test_unit"
3
+
4
+ require "oh"
5
+ require "ohbjects"
6
+
7
+ class TestOhbjects < Test::Unit::TestCase
8
+
9
+ def test_option_builder_claims_to_build_option_quote
10
+ assert Ohbjects::OptionBuilder.build?(Nokogiri(OPTION_QUOTE))
11
+ end
12
+
13
+ def test_option_builder_build
14
+ fragment = Nokogiri(OPTION_QUOTE).search(Ohbjects::OptionBuilder.spec).first
15
+
16
+ options = Ohbjects::OptionBuilder.build(fragment)
17
+
18
+ assert_equal 2, options.size, "Should have built two options"
19
+
20
+ assert_equal [Ohbjects::Put, Ohbjects::Call], options.map{|x|x.class},
21
+ "Should be a put and a call"
22
+
23
+ put = options.shift
24
+ call = options.shift
25
+
26
+ # TODO: assert individual fields
27
+
28
+ end
29
+
30
+ def test_expires_date_conversion
31
+ call = Ohbjects::Call.new
32
+ call.expires = "Jan 12, 2011"
33
+
34
+ assert_equal 12, call.expires.day
35
+ assert_equal 1, call.expires.month
36
+ assert_equal 2011, call.expires.year
37
+
38
+ assert_raise ArgumentError, "other formats should be invalid" do
39
+ call.expires = "2011-01-11"
40
+ end
41
+ end
42
+
43
+ def test_cannont_instantiate_options_directly
44
+ assert_nothing_raised "subclasses should be instantiable" do
45
+ assert_kind_of Ohbjects::Option, Ohbjects::Call.new
46
+ assert_kind_of Ohbjects::Option, Ohbjects::Put.new
47
+ end
48
+
49
+ ex = assert_raise ArgumentError do
50
+ Ohbjects::Option.new
51
+ end
52
+
53
+ assert_match %r{cannot be instantiated}, ex.message
54
+ end
55
+
56
+ OPTION_QUOTE = <<-XML
57
+ <optionQuote id="201012180000090000AA"
58
+ series="Dec 10 9.0" strikeString="9.0" optionRoot="AA"
59
+ expYear="40" expMonth="12" expDay="18" strikePrice="90000"
60
+ dte="14" expDate="Dec 18, 2010" callKey="AA:20101218:90000:C"
61
+ callBid="5.10" callAsk="5.20" callChange="0.07" callVolume="0"
62
+ callOpenInterest="38" putKey="AA:20101218:90000:P" putBid="0.00"
63
+ putAsk="0.01" putChange="0.00" putVolume="0" putOpenInterest="86"
64
+ callIv="29.1" callDelta="1" callGamma="0" callTheta="0"
65
+ callVega="0" putIv="32.2" putDelta="0" putGamma="0" putTheta="0"
66
+ putVega="0" callBidSize="821" callAskSize="1085" putBidSize="0"
67
+ putAskSize="245" trimSet="2"/>
68
+ XML
69
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oh
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 1
10
+ version: 1.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Andrew A. Smith
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-24 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: nokogiri
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rubyforge
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 7
44
+ segments:
45
+ - 2
46
+ - 0
47
+ - 4
48
+ version: 2.0.4
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: hoe
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 19
60
+ segments:
61
+ - 2
62
+ - 7
63
+ - 0
64
+ version: 2.7.0
65
+ type: :development
66
+ version_requirements: *id003
67
+ description: |-
68
+ An API for OptionsHouse (http://optionshouse.com).
69
+
70
+ Currently provides a mechanism for pulling stock and option quotes.
71
+ email:
72
+ - andy@tinnedfruit.org
73
+ executables:
74
+ - oh
75
+ extensions: []
76
+
77
+ extra_rdoc_files:
78
+ - History.txt
79
+ - Manifest.txt
80
+ files:
81
+ - .autotest
82
+ - History.txt
83
+ - Manifest.txt
84
+ - README.rdoc
85
+ - Rakefile
86
+ - bin/oh
87
+ - lib/oh.rb
88
+ - lib/ohbjects.rb
89
+ - lib/ohbjects/account.rb
90
+ - lib/ohbjects/options.rb
91
+ - lib/ohbjects/quote.rb
92
+ - test/test_oh.rb
93
+ - test/test_ohbjects.rb
94
+ - test/test_options.rb
95
+ has_rdoc: true
96
+ homepage: http://github.com/aasmith/oh
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options:
101
+ - --main
102
+ - README.rdoc
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ hash: 3
120
+ segments:
121
+ - 0
122
+ version: "0"
123
+ requirements: []
124
+
125
+ rubyforge_project: oh
126
+ rubygems_version: 1.3.7
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: An API for OptionsHouse (http://optionshouse.com)
130
+ test_files:
131
+ - test/test_options.rb
132
+ - test/test_oh.rb
133
+ - test/test_ohbjects.rb