stockfighter 0.3.1 → 0.4.0

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