chock_a_block 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 61eac32feffc31892a866b93630c02d84274154f
4
+ data.tar.gz: 677f850db05f1cb59da016806a43095f0ce4f250
5
+ SHA512:
6
+ metadata.gz: a4877e9e853be54c86325aa1e53e97185c06c0bc0ba5f02f6aa5e678fb0feb1d10a0725d900b8a5d363bc50ff29397c43c9d94362c09dd6a0b3f609a6bcae371
7
+ data.tar.gz: d259a1b9a20153211ab925f07c1b411ed4bbc8e156a7ed16a18d7f6a0dccaa13af506b1c1ae0139088b34177577c41f25cc22577585debe6a67cf4bd4df0d668
@@ -0,0 +1,87 @@
1
+ module ChockABlock
2
+ class AlgoTrader
3
+ include Errors
4
+
5
+ # The total amount of stocks to buy
6
+ TOTAL_AMOUNT = 100_000
7
+
8
+ def initialize(api_key, account, venue, symbol)
9
+ @currntly_bought = 0
10
+ @last_bidding_price = Float::INFINITY
11
+ @stock = Stock.new(venue, symbol)
12
+ @broker = Broker.new(api_key, account, venue, symbol)
13
+ end
14
+
15
+ def basic_strategy
16
+ # Use a ticker to reset orders every 5 times
17
+ # to avoid sticking with very low bidding price
18
+ ticker = 0
19
+ success_ticker = 0
20
+
21
+ while TOTAL_AMOUNT > @currntly_bought
22
+ begin
23
+ # Get best bidding price/qty from stock orderbook
24
+ bid = @stock.best_bidding
25
+
26
+ # reset every 10_000 attempts to avoid stalling for too long
27
+ # NOTE: this is not important and the game can be finished without it
28
+ # NOTE: but it increases the success rate of the algorithm
29
+ ticker += 1
30
+ if ticker > 10_000
31
+ ticker = 0
32
+ orders = @broker.get_orders
33
+ update_status(orders)
34
+ cancel_orders_and_reset_bidding_price(orders)
35
+ end
36
+
37
+ # Place order if price market price is reasonable
38
+ # and the current bidding price offered is lower than our last price
39
+ next if @last_bidding_price < bid['price']
40
+ @broker.place_order(bid['price'] + 1, bid['qty'])
41
+ @last_bidding_price = bid['price']
42
+ sleep 2
43
+
44
+ # Get Status about current orders count
45
+ orders = @broker.get_orders
46
+ update_status(orders)
47
+
48
+ # reset every 10 successful attempts to avoid flooding bidding size
49
+ # NOTE: this is not important and the game can be finished without it
50
+ # NOTE: but it increases the success rate of the algorithm
51
+ success_ticker += 1
52
+ cancel_orders_and_reset_bidding_price(orders) if success_ticker % 10 == 0
53
+ rescue ChockABlock::OrderError => e
54
+ p 'Something went wrong while placing order'
55
+ p e.message
56
+ p 'Retrying'
57
+ retry
58
+ rescue ChockABlock::StockNotFoundError, ChockABlock::VenueNotFoundError => e
59
+ # when the game ends the Venue and Stock are removed so
60
+ # any request done after that return
61
+ p 'Sorry, The game ended before we finish, :('
62
+ p '--------------------------------------------------------'
63
+ return
64
+ end
65
+ end
66
+ p 'Congratulations, We successfully finished :D'
67
+ p '--------------------------------------------------------'
68
+ end
69
+
70
+ # calculate the sum of currently bought stocks from orders
71
+ # and print out the current progress
72
+ def update_status(orders)
73
+ @currntly_bought = orders.map{|order| order['totalFilled']}.inject :+
74
+ # print progress
75
+ p "Progress: #{(@currntly_bought.to_f / TOTAL_AMOUNT.to_f * 100.0).round(1)}%"
76
+ end
77
+
78
+ # cancel all orders after certain ticks to avoid pushing the market
79
+ # into the higher price.
80
+ # and set the last bidding price high to avoid stucking with a very
81
+ # low bidding price.
82
+ def cancel_orders_and_reset_bidding_price(orders)
83
+ @last_bidding_price = Float::INFINITY
84
+ orders.each{|order| @broker.cancel_order(order['id'])}
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,85 @@
1
+ module ChockABlock
2
+ class Broker
3
+ include Errors
4
+
5
+ # Initialize a Broker to interact with API
6
+ # placing orders and getting order stats... etc.
7
+ #
8
+ # @param api_key [String] API auth key
9
+ # @param account [String] account id for given game
10
+ # @param venue [String] venue name
11
+ # @param symbol [String] stock symbol
12
+ def initialize(api_key, account, venue, symbol)
13
+ @api_key = api_key
14
+ @account = account
15
+ @venue = venue
16
+ @symbol = symbol
17
+ end
18
+
19
+ # Place an order
20
+ # API Consumer
21
+ #
22
+ # @param price [Integer] price for buying
23
+ # @param qty [Integer] quantity wanted to buy
24
+ #
25
+ # @return [Hash] representing the placed order
26
+ def place_order(price, qty)
27
+ response = HTTP['X-Starfighter-Authorization' => @api_key].post(
28
+ BASE_URL + "/venues/#{@venue}/stocks/#{@symbol}/orders",
29
+ json: {
30
+ account: @account,
31
+ venue: @venue,
32
+ stock: @symbol,
33
+ price: price,
34
+ qty: qty,
35
+ direction: :buy,
36
+ orderType: :limit
37
+ }
38
+ )
39
+ response_body = JSON.parse response.body
40
+ fail AuthenticationError, @api_key if response.code == 401
41
+ fail OrderError, response_body['error'] unless response_body['ok']
42
+ response_body
43
+ end
44
+
45
+ # Get all the orders of a specific stock
46
+ # API Consumer
47
+ #
48
+ # @return [Hash] representing the order in query
49
+ def get_orders
50
+ response = HTTP['X-Starfighter-Authorization' => @api_key].get(
51
+ BASE_URL + "/venues/#{@venue}/accounts/#{@account}/stocks/#{@symbol}/orders"
52
+ )
53
+ fail AuthenticationError, @api_key if response.code == 401
54
+ JSON.parse(response.body)['orders']
55
+ end
56
+
57
+ # Get specific order details
58
+ # API Consumer
59
+ #
60
+ # @params id [Integer] representing the order id to query
61
+ #
62
+ # @return [Hash] representing the order in query
63
+ def get_order(id)
64
+ response = HTTP['X-Starfighter-Authorization' => @api_key].get(
65
+ BASE_URL + "/venues/#{@venue}/stocks/#{@symbol}/orders/#{id}"
66
+ )
67
+ fail AuthenticationError, @api_key if response.code == 401
68
+ JSON.parse response.body
69
+ end
70
+
71
+ # Cancel specific order details
72
+ # API Consumer
73
+ #
74
+ # @params id [Integer] representing the order id to cancel
75
+ #
76
+ # @return [Hash] representing the order cancelled
77
+ def cancel_order(id)
78
+ response = HTTP['X-Starfighter-Authorization' => @api_key].delete(
79
+ BASE_URL + "/venues/#{@venue}/stocks/#{@symbol}/orders/#{id}"
80
+ )
81
+ fail AuthenticationError, @api_key if response.code == 401
82
+ JSON.parse response.body
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ module ChockABlock
2
+ module Errors
3
+ # Base Error
4
+ class ChockABlockError < StandardError
5
+ end
6
+
7
+ # 401 error
8
+ class AuthenticationError < ChockABlockError
9
+ end
10
+
11
+ # Error when stock symbol is not found
12
+ class VenueNotFoundError < ChockABlockError
13
+ end
14
+
15
+ # Error when stock symbol is not found
16
+ class StockNotFoundError < ChockABlockError
17
+ end
18
+
19
+ # Error when placing order fails
20
+ class OrderError < ChockABlockError
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,50 @@
1
+ module ChockABlock
2
+ class Stock
3
+ include Errors
4
+
5
+ # Initialize a Stock resource API consumer
6
+ #
7
+ # @param venue [String] venue name
8
+ # @param symbol [String] stock symbol
9
+ def initialize(venue, symbol)
10
+ @venue = venue
11
+ @symbol = symbol
12
+ end
13
+
14
+ # Get the orderbook for specific STOCK_SYMBOL in a specific VENUE
15
+ # API Consumer
16
+ #
17
+ # @return [Hash] representing the response of the stock orderbook
18
+ def get_orderbook
19
+ response = HTTP.get(BASE_URL + "/venues/#{@venue}/stocks/#{@symbol}")
20
+ fail StockNotFoundError, @symbol if response.code == 500
21
+ fail VenueNotFoundError, @venue if response.code == 404
22
+ JSON.parse response.body
23
+ end
24
+
25
+ # Get the qoute for a specific STOCK_SYMBOL in a specifc VENUE
26
+ # API Consumer
27
+ #
28
+ # @return [Hash] representing the response of the stock quote
29
+ def get_quote
30
+ response = HTTP.get(BASE_URL + "/venues/#{@venue}/stocks/#{@symbol}/quote")
31
+ fail StockNotFoundError, @symbol if response.code == 404
32
+ JSON.parse response.body
33
+ end
34
+
35
+ # Get the best bidding currently available on the orderbook
36
+ #
37
+ # @return [Hash] representing the best current price/qty offered by bots
38
+ # or retry if best_bid is nil
39
+ def best_bidding
40
+ @order_book = get_orderbook
41
+ best_bid = @order_book['bids']&.last
42
+
43
+ return best_bid unless best_bid.nil?
44
+
45
+ # retryl few seconds later
46
+ sleep 2
47
+ best_bidding
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ # Require 3rd party dependencies
2
+ require 'json'
3
+ require 'http'
4
+
5
+ # Require relative core classes (dependencies)
6
+ require_relative 'chock_a_block/errors'
7
+ require_relative 'chock_a_block/stock'
8
+ require_relative 'chock_a_block/broker'
9
+ require_relative 'chock_a_block/algo_trader'
10
+
11
+ # The Base Module to solve chock_a_block challange on stockerfighter.io
12
+ # Provide easy way of consuming the api to start level
13
+ # and configure the correct venue, stock and account
14
+ module ChockABlock
15
+ include Errors
16
+
17
+ BASE_URL = 'https://api.stockfighter.io/ob/api'
18
+ GAME_URL = 'https://www.stockfighter.io/gm/'
19
+
20
+ # Provide an easy way to start the game consuming the api
21
+ # instead of copying and pasting the account/venue/symbol of each game
22
+ def self.start_level(api_key)
23
+ response = HTTP['X-Starfighter-Authorization' => api_key].post(
24
+ GAME_URL + 'levels/chock_a_block'
25
+ )
26
+ response_body = JSON.parse response.body
27
+ fail AuthenticationError, api_key unless response_body['ok']
28
+
29
+ # Get the level data using the undocumented API here
30
+ # https://discuss.starfighters.io/t/the-gm-api-how-to-start-stop-restart-resume-trading-levels-automagically/143
31
+ @accout = response_body['account']
32
+ @venue = response_body['venues'].first
33
+ @symbol = response_body['tickers'].first
34
+ @instance = response_body['instanceId']
35
+
36
+ p 'Game Started'
37
+ p 'Please Navigate to:'
38
+ p ' https://www.stockfighter.io/ui/play/blotter#chock_a_block'
39
+ p 'to see the game in action'
40
+ p '--------------------------------------------------------'
41
+ # Start the AlgoTrader
42
+ AlgoTrader.new(api_key, @accout, @venue, @symbol).basic_strategy
43
+ rescue AuthenticationError => e
44
+ p "Invalid API_KEY #{e.message}"
45
+ p 'Please use a correct API_KEY to start the level'
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe ChockABlock do
4
+ it 'has a version number' do
5
+ expect(ChockABlock::VERSION).not_to be nil
6
+ end
7
+
8
+ it 'does something useful' do
9
+ expect(false).to eq(true)
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'chock_a_block'
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chock_a_block
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ahmed Abdel-Razzak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: vcr
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: http
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - abdelrazzak.ahmed@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - lib/chock_a_block.rb
133
+ - lib/chock_a_block/algo_trader.rb
134
+ - lib/chock_a_block/broker.rb
135
+ - lib/chock_a_block/errors.rb
136
+ - lib/chock_a_block/stock.rb
137
+ - spec/chock_a_block_spec.rb
138
+ - spec/spec_helper.rb
139
+ homepage: https://github.com/artmees/chock_a_block
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 2.3.0
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.5.1
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Solution to chock_a_block
163
+ test_files: []
164
+ has_rdoc: