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 +7 -0
- data/lib/chock_a_block/algo_trader.rb +87 -0
- data/lib/chock_a_block/broker.rb +85 -0
- data/lib/chock_a_block/errors.rb +23 -0
- data/lib/chock_a_block/stock.rb +50 -0
- data/lib/chock_a_block.rb +47 -0
- data/spec/chock_a_block_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +164 -0
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
|
data/spec/spec_helper.rb
ADDED
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:
|