oh 1.0.1

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