stockfighter 0.3.1 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 68adc328d3d355eff80ad92e8254f5e96ce8a0f1
4
- data.tar.gz: b3ee6f33a1e53a8fe9661e5708c67c21f24b642b
3
+ metadata.gz: 732fe76a171ed98a78d642e01b24606254ca687b
4
+ data.tar.gz: d2798df2beb98d49509180d44196ec8a0ed6d444
5
5
  SHA512:
6
- metadata.gz: 600d0eb1c467c8292b9dfe1b914e78092a885f109cf70018026c2500b578e8342fdd060d52a38237946ec92d6e91e5fa094f8c0eda93d075eb97db6044a7602f
7
- data.tar.gz: a021f108dacd96883bb97eb513ddbb06972b6fb408fea03c602e0cf2ce6ef0985160c4bbd4c6fac24ee661bddf2be01537eecccdb89619a0a9ee9b2334c998d1
6
+ metadata.gz: 5f44263691227792ed908acc7f9eb009889bcf4b3f2dbe936499cab18ef3ca3632a1cb04f4016ee091da2f763cf316baaf753663d363f303b11608f8d5b56f2d
7
+ data.tar.gz: dd09455db5254b0d77438738a3b35cde95f66ffcd8cd7562e8136028706d939bf8626568f29760fbd545f458a0535f7d7c80ab3cac23d039d506a4149effabaf
data/README.md CHANGED
@@ -43,22 +43,33 @@ gm = Stockfighter::GM.new(key: "supersecretapikey1234567", level: "first_steps")
43
43
 
44
44
  api = Stockfighter::Api.new(gm.config)
45
45
 
46
- # Use the GM to register message callbacks for messages received from the GM. The GM needs to be initialized with polling: true to set up polling of the GM and enable callbacks.
46
+ # Use the GM to register message callbacks for messages received & trading day notification from the GM.
47
+ # The GM needs to be initialized with polling: true to set up polling of the GM and enable callbacks.
47
48
 
48
49
  gm = Stockfighter::GM.new(key: "supersecretapikey1234567", level: "first_steps", polling: true)
49
50
 
50
- gm.add_message_callback('success') { |message|
51
- puts "\e[#32m#{message}\e[0m"
52
- }
53
- gm.add_message_callback('info') { |message|
54
- puts "\e[#34m#{message}\e[0m"
51
+ ansi_code = Hash.new
52
+ ansi_code['success'] = "\e[#32m"
53
+ ansi_code['info'] = "\e[#34m"
54
+ ansi_code['warning'] = "\e[#33m"
55
+ ansi_code['error'] = "\e[#31m"
56
+ ansi_code['danger'] = "\e[#31m"
57
+
58
+ gm.add_message_callback { |type,message|
59
+ abort("Unhandled message type #{type}") unless ansi_code.key?(type)
60
+ puts "#{ansi_code[type]}#{message}\e[0m"
55
61
  }
62
+
56
63
  gm.add_state_change_callback { |previous_state, new_state|
57
64
  if new_state == 'won'
58
65
  puts "You've won!"
59
66
  end
60
67
  }
61
68
 
69
+ gm.add_trading_day_callback { |previous_trading_day, current_trading_day, end_of_the_world_day|
70
+ # Due to the relatively infrequent polling (to avoid rate limiting), the callback will not be called on every individual trading day
71
+ }
72
+
62
73
  # Restart the level
63
74
 
64
75
  gm.restart
@@ -120,19 +131,37 @@ api = Stockfighter::Api.new(key: key, account: account, symbol: symbol, venue: v
120
131
  ```ruby
121
132
  websockets = Stockfighter::Websockets.new(gm.config)
122
133
  websockets.add_quote_callback { |quote|
134
+ # Ensure you don't have long running operations (eg calling api.*) as part of this
135
+ # callback method as the event processing for all websockets is performed on 1 thread.
123
136
  puts quote
124
137
  }
138
+
125
139
  websockets.add_execution_callback { |execution|
140
+ # Ensure you don't have long running operations (eg calling api.*) as part of this
141
+ # callback method as the event processing for all websockets is performed on 1 thread.
126
142
  puts execution
127
143
  }
144
+
128
145
  websockets.start()
146
+
147
+ # The tickertape websocket can be optionally disabled when calling start()
148
+
149
+ websockets = Stockfighter::Websockets.new(gm.config)
150
+ websockets.add_execution_callback { |execution|
151
+ puts execution
152
+ }
153
+
154
+ websockets.start(tickertape_enabled:false)
155
+
156
+ # The executions websocket can also be disabled by passing executions_enabled:false when calling start()
157
+
129
158
  ```
130
159
 
131
160
  ## Todo
132
161
 
133
162
  * ~~TODO: Usage instructions!~~
134
163
  * ~~TODO: Game master integration~~
135
- * TODO: Tests
164
+ * TODO: Tests!
136
165
  * TODO: Error Handling (partially complete)
137
166
 
138
167
  ## Contributing
@@ -141,4 +170,7 @@ websockets.start()
141
170
  2. Create your feature branch (`git checkout -b my-new-feature`)
142
171
  3. Commit your changes (`git commit -am 'Add some feature'`)
143
172
  4. Push to the branch (`git push origin my-new-feature`)
144
- 5. Create a new Pull Request
173
+ 5. Write some tests
174
+ 6. Run all the tests using the following command:
175
+ `API_KEY="insert_your_api_key_here" rake test`
176
+ 7. Create a new Pull Request
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "minitest"
2
4
 
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
@@ -2,9 +2,8 @@ require 'httparty'
2
2
 
3
3
  module Stockfighter
4
4
  class Api
5
- BASE_URL = "https://api.stockfighter.io/ob/api"
6
-
7
- def initialize(key:, account:, symbol:, venue:)
5
+ def initialize(key:, account:, symbol:, venue:, base_url: "https://api.stockfighter.io/ob/api")
6
+ @base_url = base_url
8
7
  @api_key = key
9
8
  @account = account
10
9
  @symbol = symbol
@@ -12,7 +11,7 @@ module Stockfighter
12
11
  end
13
12
 
14
13
  def get_quote
15
- HTTParty.get("#{BASE_URL}/venues/#{@venue}/stocks/#{@symbol}/quote", auth_header).parsed_response
14
+ perform_request("get", "#{@base_url}/venues/#{@venue}/stocks/#{@symbol}/quote")
16
15
  end
17
16
 
18
17
  def place_order(price:, quantity:, direction:, order_type:)
@@ -25,37 +24,49 @@ module Stockfighter
25
24
  "direction" => direction,
26
25
  "orderType" => order_type
27
26
  }
28
-
29
- HTTParty.post("#{BASE_URL}/venues/#{@venue}/stocks/#{@symbol}/orders", body: JSON.dump(order),
30
- headers: auth_header).parsed_response
27
+ perform_request("post", "#{@base_url}/venues/#{@venue}/stocks/#{@symbol}/orders", body: JSON.dump(order))
31
28
  end
32
29
 
33
30
  def cancel_order(order_id)
34
- HTTParty.delete("#{BASE_URL}/venues/#{@venue}/stocks/#{@symbol}/orders/#{order_id}", headers: auth_header)
31
+ perform_request("delete", "#{@base_url}/venues/#{@venue}/stocks/#{@symbol}/orders/#{order_id}")
35
32
  end
36
33
 
37
34
  def order_status(order_id)
38
- HTTParty.get("#{BASE_URL}/venues/#{@venue}/stocks/#{@symbol}/orders/#{order_id}", headers: auth_header).parsed_response
35
+ perform_request("get", "#{@base_url}/venues/#{@venue}/stocks/#{@symbol}/orders/#{order_id}")
39
36
  end
40
37
 
41
38
  def order_book
42
- HTTParty.get("#{BASE_URL}/venues/#{@venue}/stocks/#{@symbol}", headers: auth_header).parsed_response
39
+ perform_request("get", "#{@base_url}/venues/#{@venue}/stocks/#{@symbol}")
43
40
  end
44
41
 
45
42
  def venue_up?
46
- response = HTTParty.get("#{BASE_URL}/venues/#{@venue}/heartbeat", headers: auth_header).parsed_response
43
+ response = perform_request("get", "#{@base_url}/venues/#{@venue}/heartbeat")
47
44
  response["ok"]
48
45
  end
49
46
 
50
47
  def status_all
51
- HTTParty.get("#{BASE_URL}/venues/#{@venue}/accounts/#{@account}/orders", headers: auth_header)
48
+ perform_request("get", "#{@base_url}/venues/#{@venue}/accounts/#{@account}/orders")
52
49
  end
53
50
 
54
- def auth_header
55
- {"X-Starfighter-Authorization" => @api_key}
56
- end
51
+ def perform_request(action, url, body:nil)
52
+ options = {
53
+ :headers => {"X-Starfighter-Authorization" => @api_key},
54
+ :format => :json
55
+ }
56
+ if body != nil
57
+ options[:body] = body
58
+ end
59
+ response = HTTParty.method(action).call(url, options)
57
60
 
58
- private :auth_header
61
+ if response.code == 200 and response["ok"]
62
+ response
63
+ elsif not response["ok"]
64
+ raise "Error response received from #{url}: #{response['error']}"
65
+ else
66
+ raise "HTTP error response received from #{url}: #{response.code}"
67
+ end
68
+ end
59
69
 
70
+ private :perform_request
60
71
  end
61
72
  end
@@ -11,12 +11,13 @@ module Stockfighter
11
11
  @api_key = key
12
12
  @level = level
13
13
 
14
- @callback_types = ['success', 'info']
15
- @message_callbacks = Hash.new { |h,k| h[k] = [] }
14
+ @message_callbacks = []
16
15
  @state_change_callbacks = []
16
+ @trading_day_callbacks = []
17
17
 
18
18
  new_level_response = perform_request("post", "#{GM_URL}/levels/#{level}")
19
19
  previous_state = new_level_response['state']
20
+ previous_trading_day = 0
20
21
 
21
22
  if polling
22
23
  # websocket API functionality instead of polling would be great here
@@ -31,19 +32,34 @@ module Stockfighter
31
32
  }
32
33
  previous_state = current_state
33
34
  end
35
+
36
+ if response.key?('details')
37
+ details = response['details']
38
+ current_trading_day = details['tradingDay']
39
+ end_of_the_world_day = details['endOfTheWorldDay']
40
+ if previous_trading_day != current_trading_day
41
+ @trading_day_callbacks.each { |callback|
42
+ callback.call(previous_trading_day, current_trading_day, end_of_the_world_day)
43
+ }
44
+ previous_trading_day = current_trading_day
45
+ end
46
+ end
34
47
  end
35
48
  end
36
49
  end
37
50
 
38
- def add_message_callback(type, &block)
39
- raise "Unknown message callback type #{type}" unless @callback_types.include? type
40
- @message_callbacks[type] << block
51
+ def add_message_callback(&block)
52
+ @message_callbacks << block
41
53
  end
42
54
 
43
55
  def add_state_change_callback(&block)
44
56
  @state_change_callbacks << block
45
57
  end
46
58
 
59
+ def add_trading_day_callback(&block)
60
+ @trading_day_callbacks << block
61
+ end
62
+
47
63
  def config
48
64
  if @config[:account] && @config[:venue] && @config[:symbol]
49
65
  @config
@@ -74,7 +90,11 @@ module Stockfighter
74
90
  end
75
91
 
76
92
  def perform_request(action, url)
77
- response = HTTParty.method(action).call(url, :headers => {"X-Starfighter-Authorization" => @api_key})
93
+ options = {
94
+ :headers => {"X-Starfighter-Authorization" => @api_key},
95
+ :format => :json
96
+ }
97
+ response = HTTParty.method(action).call(url, options)
78
98
  if response.code != 200
79
99
  raise "HTTP error response received from #{url}: #{response.code}"
80
100
  end
@@ -84,17 +104,11 @@ module Stockfighter
84
104
 
85
105
  if response.key?('flash')
86
106
  flash = response['flash']
87
- if flash.key?('success')
88
- @message_callbacks['success'].each { |callback|
89
- callback.call(flash['success'])
107
+ flash.each { |type,message|
108
+ @message_callbacks.each { |callback|
109
+ callback.call(type, message)
90
110
  }
91
- elsif flash.key?('info')
92
- @message_callbacks['info'].each { |callback|
93
- callback.call(flash['info'])
94
- }
95
- else
96
- raise "TODO: Unhandled flash scenario: #{response}"
97
- end
111
+ }
98
112
  end
99
113
 
100
114
  if response.key?('instructions')
@@ -1,3 +1,3 @@
1
1
  module Stockfighter
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -13,7 +13,7 @@ module Stockfighter
13
13
  @execution_callbacks = []
14
14
  end
15
15
 
16
- def start()
16
+ def start(tickertape_enabled:true, executions_enabled:true)
17
17
 
18
18
  EM.epoll
19
19
  EM.run do
@@ -22,71 +22,85 @@ module Stockfighter
22
22
  abort("Error raised during event loop: #{e}")
23
23
  }
24
24
 
25
- tickertape = WebSocket::EventMachine::Client.connect(:uri => "#{WS_URL}/#{@account}/venues/#{@venue}/tickertape", :ssl => true)
26
- tickertape.onopen do
27
- puts "tickertape websocket: connected"
28
- end
25
+ if tickertape_enabled
26
+ tickertape_options = {
27
+ :uri => "#{WS_URL}/#{@account}/venues/#{@venue}/tickertape",
28
+ :ssl => true
29
+ }
29
30
 
30
- tickertape.onmessage do |msg|
31
- incoming = JSON.parse(msg)
32
- if not incoming["ok"]
33
- raise "tickertape websocket: Error response received: #{msg}"
31
+ tickertape = WebSocket::EventMachine::Client.connect(tickertape_options)
32
+ tickertape.onopen do
33
+ puts "#{@account} tickertape websocket: connected"
34
34
  end
35
- if incoming.key?('quote')
36
- quote = incoming['quote']
37
- @quote_callbacks.each { |callback|
38
- callback.call(quote)
39
- }
40
- else
41
- raise "tickertape websocket: TODO: Unhandled message type: #{msg}"
35
+
36
+ tickertape.onmessage do |msg|
37
+ incoming = JSON.parse(msg)
38
+ if not incoming["ok"]
39
+ raise "#{@account} tickertape websocket: Error response received: #{msg}"
40
+ end
41
+ if incoming.key?('quote')
42
+ quote = incoming['quote']
43
+ @quote_callbacks.each { |callback|
44
+ callback.call(quote)
45
+ }
46
+ else
47
+ raise "#{@account} tickertape websocket: TODO: Unhandled message type: #{msg}"
48
+ end
42
49
  end
43
- end
44
50
 
45
- tickertape.onerror do |e|
46
- puts "tickertape websocket: Error #{e}"
47
- end
51
+ tickertape.onerror do |e|
52
+ puts "#{@account} tickertape websocket: Error #{e}"
53
+ end
48
54
 
49
- tickertape.onping do |msg|
50
- puts "tickertape websocket: Received ping: #{msg}"
51
- end
55
+ tickertape.onping do |msg|
56
+ puts "#{@account} tickertape websocket: Received ping: #{msg}"
57
+ end
52
58
 
53
- tickertape.onpong do |msg|
54
- puts "tickertape websocket: Received pong: #{msg}"
55
- end
59
+ tickertape.onpong do |msg|
60
+ puts "#{@account} tickertape websocket: Received pong: #{msg}"
61
+ end
56
62
 
57
- tickertape.onclose do
58
- raise "tickertape websocket: Disconnected"
63
+ tickertape.onclose do |code, reason|
64
+ raise "#{@account} tickertape websocket: Client disconnected with status code: #{code} and reason: #{reason}"
65
+ end
59
66
  end
60
67
 
61
- executions = WebSocket::EventMachine::Client.connect(:uri => "#{WS_URL}/#{@account}/venues/#{@venue}/executions", :ssl => true)
62
- executions.onopen do
63
- puts "executions websocket: Connected"
64
- end
68
+ if executions_enabled
69
+ executions_options = {
70
+ :uri => "#{WS_URL}/#{@account}/venues/#{@venue}/executions",
71
+ :ssl => true
72
+ }
65
73
 
66
- executions.onmessage do |msg|
67
- execution = JSON.parse(msg)
68
- if not execution["ok"]
69
- raise "execution websocket: Error response received: #{msg}"
74
+ executions = WebSocket::EventMachine::Client.connect(executions_options)
75
+ executions.onopen do
76
+ puts "#{@account} executions websocket: connected"
70
77
  end
71
- @execution_callbacks.each { |callback|
72
- callback.call(execution)
73
- }
74
- end
75
78
 
76
- executions.onerror do |e|
77
- puts "executions websocket: Error: #{e}"
78
- end
79
+ executions.onmessage do |msg|
80
+ execution = JSON.parse(msg)
81
+ if not execution["ok"]
82
+ raise "#{@account} execution websocket: Error response received: #{msg}"
83
+ end
84
+ @execution_callbacks.each { |callback|
85
+ callback.call(execution)
86
+ }
87
+ end
79
88
 
80
- executions.onping do |msg|
81
- puts "executions websocket: Received ping: #{msg}"
82
- end
89
+ executions.onerror do |e|
90
+ puts "#{@account} executions websocket: Error: #{e}"
91
+ end
83
92
 
84
- executions.onpong do |msg|
85
- puts "executions websocket: Received pong: #{msg}"
86
- end
93
+ executions.onping do |msg|
94
+ puts "#{@account} executions websocket: Received ping: #{msg}"
95
+ end
87
96
 
88
- executions.onclose do
89
- raise "executions websocket: Disconnected"
97
+ executions.onpong do |msg|
98
+ puts "#{@account} executions websocket: Received pong: #{msg}"
99
+ end
100
+
101
+ executions.onclose do |code, reason|
102
+ raise "#{@account} executions websocket: Client disconnected with status code: #{code} and reason: #{reason}"
103
+ end
90
104
  end
91
105
  end
92
106
  end
data/stockfighter.gemspec CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.add_runtime_dependency "websocket-eventmachine-client", "~> 1.1"
24
24
  spec.add_development_dependency "bundler", "~> 1.7"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest", "~> 5.8.3"
26
27
  end
data/test/test_api.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'minitest/autorun'
2
+ require 'stockfighter'
3
+
4
+ class ApiTest < Minitest::Unit::TestCase
5
+ def test_get_quote
6
+ api = get_api()
7
+
8
+ quote = api.get_quote()
9
+ assert_equal 'FOOBAR', quote['symbol']
10
+ assert_equal 'TESTEX', quote['venue']
11
+ assert quote.key?('quoteTime')
12
+ end
13
+
14
+ def test_place_order_invalid_scenarios
15
+ api = get_api()
16
+
17
+ assert_raises(RuntimeError) {
18
+ api.place_order(price:10000000, quantity:100, direction:'invalid_direction', order_type:'limit')
19
+ }
20
+ assert_raises(RuntimeError) {
21
+ api.place_order(price:10000000, quantity:100, direction:'buy', order_type:'invalid_order_type')
22
+ }
23
+ assert_raises(RuntimeError) {
24
+ api.place_order(price:-1, quantity:100, direction:'buy', order_type:'limit')
25
+ }
26
+ assert_raises(RuntimeError) {
27
+ api.place_order(price:10000000, quantity:-1, direction:'buy', order_type:'limit')
28
+ }
29
+ end
30
+
31
+ def test_place_order_happy_day
32
+ api = get_api()
33
+
34
+ order = api.place_order(price:10, quantity:100, direction:'sell', order_type:'limit')
35
+ assert_equal 'FOOBAR', order['symbol']
36
+ assert_equal 'TESTEX', order['venue']
37
+ assert_equal 'sell', order['direction']
38
+ assert_equal 10, order['price']
39
+ assert_equal 'limit', order['orderType']
40
+ assert_equal 'EXB123456', order['account']
41
+
42
+ assert order.key?('ts')
43
+ assert order.key?('fills')
44
+ end
45
+
46
+ def test_cancel_order_invalid_scenarios
47
+ api = get_api()
48
+
49
+ assert_raises(RuntimeError) {
50
+ api.cancel_order(1)
51
+ }
52
+ end
53
+
54
+ def test_cancel_order_happy_day
55
+ api = get_api()
56
+
57
+ order = api.place_order(price:1, quantity:1000000, direction:'buy', order_type:'limit')
58
+ assert order['open']
59
+
60
+ cancel_response = api.cancel_order(order['id'])
61
+ assert !cancel_response['open']
62
+ end
63
+
64
+ def get_api
65
+ api_key = ENV['API_KEY']
66
+ assert api_key.to_s != '', "export API_KEY='secret' before running these tests, where 'secret' is your API key"
67
+
68
+ config = {
69
+ :key => api_key,
70
+ :account => 'EXB123456',
71
+ :venue => 'TESTEX',
72
+ :symbol => 'FOOBAR',
73
+ }
74
+ Stockfighter::Api.new(config)
75
+ end
76
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stockfighter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert J Samson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-22 00:00:00.000000000 Z
11
+ date: 2016-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 5.8.3
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 5.8.3
83
97
  description: An API wrapper for Starfighter's Stockfighter - see www.stockfighter.io
84
98
  email:
85
99
  - rjsamson@me.com
@@ -98,6 +112,7 @@ files:
98
112
  - lib/stockfighter/version.rb
99
113
  - lib/stockfighter/websockets.rb
100
114
  - stockfighter.gemspec
115
+ - test/test_api.rb
101
116
  homepage: https://github.com/rjsamson/stockfighter
102
117
  licenses:
103
118
  - MIT
@@ -122,4 +137,5 @@ rubygems_version: 2.4.5
122
137
  signing_key:
123
138
  specification_version: 4
124
139
  summary: An API wrapper for Starfighter's Stockfighter
125
- test_files: []
140
+ test_files:
141
+ - test/test_api.rb