Skip to content

Instantly share code, notes, and snippets.

@Adamantish
Last active February 25, 2016 13:58
Show Gist options
  • Save Adamantish/665fa6a15bfeae725206 to your computer and use it in GitHub Desktop.
Save Adamantish/665fa6a15bfeae725206 to your computer and use it in GitHub Desktop.
Consumes the https://www.stockfighter.io/ simulated market API if given non-expired account codes
require 'httparty'
require 'json'
# require 'yaml'
# dearest_secrets = YAML.load_file('my_dear_secrets.yml')
## Nothing really to be lost by publicly sharing this key
API_KEY = "2e5dd4767307806ae789ce47943c5e880a04fd9c"
ACCOUNT = "LAK66585141"
VENUE = "QEOLEX"
STOCK = "BSH"
class Position
SHARES_TO_BUY = 100_000
ORDER_EXPIRY_SECONDS = 90
attr_reader :my_orders, :shares_remaining, :unfilled_bid_value
def initialize(api: )
@shares_remaining = SHARES_TO_BUY
@api = api
@my_orders = Hash.new{ |hash, key| hash[key] = {} }
@unfilled_bid_value = 0
@ignore_unexpired_orders = Proc.new { | order |
expire_time = Time.now - ORDER_EXPIRY_SECONDS
!order["open"] || Time.parse( order["ts"] ) >= expire_time
}
@ignore_closed_orders = Proc.new { | order| ! order["open"] }
end
def record( response )
@my_orders[response["id"]] = response
if response["direction"] == "buy"
bid_value_change = response["open"] ? 1 : -1
end
puts "Recorded: " + response.to_s
puts "Shares sold: " + orders_sum('totalFilled').to_s
puts "Open orders: " + my_open_orders.count.to_s
end
def act( direction, qty, price, id = nil, orderType = "limit" )
# do not allow unfilled orders to be more than a certain value. Don't make more orders until it falls below threshold.
if direction == "cancel"
record ( @api.write( direction, nil, nil, id ))
end
record ( @api.write( direction, qty, price, id, orderType ) )
end
def learn( method , id = nil )
if method == :order
record( @api.read( method , id ) )
else
@api.read( method , id )
end
end
def my_open_orders ; select_orders( &@ignore_closed_orders ) ; end
def cancel_expired_orders; cancel_orders( @ignore_unexpired_orders ) ;end
# Having to iterate like this because it seems that the broader API call gets all the closed orders too.
def get_open_order_statuses
my_open_orders.each_pair do | id, order |
order_status id
end
end
def order_status ( id )
response = @api.read( :order, id )
@my_orders[id] = response
fills_remaining = response["originalQty"] - response["totalFilled"]
puts "Order #{id} has #{fills_remaining} fills remaining"
end
private
def cancel_orders( reject_condition )
orders = select_orders( &reject_condition )
orders.each_pair { | id, order | act("cancel" , nil, nil, id ) }
end
def orders_sum( numeric_field )
@my_orders.inject(0) do | acc, (id, order) |
reject = yield( order ) if block_given?
reject ? acc : acc + order[numeric_field]
end
end
def select_orders
@my_orders.select do | id, order |
!yield( order )
end
end
end
## ----------------------------------------------------------------------------
class SFApi
HEADERS = {"X-Starfighter-Authorization" => API_KEY}
BASE_URL = "https://api.stockfighter.io/ob/api"
attr_reader :orders, :order, :quote, :asks, :bids
def initialize( account, venues, ticker )
@account = account
@venues = venues
@ticker = ticker
make_urls
end
# The 'method' argument maps to a key in the @urls hash *and* an attr_reader in this class for storing the response.
def read ( method , id = nil )
instance_variable_set( "@#{method}".to_sym , rest( :get, method , id ) )
if @orders
@asks, @bids = [ @orders["asks"] , @orders["bids"] ]
end
method(method).call
end
def write ( direction, qty, price, id = nil, orderType = "limit" )
body = {
"orderType" => orderType,
"qty" => qty,
"price" => price,
"direction" => direction,
"account" => @account
}
case direction
when "cancel"
rest( :delete, :cancel, id )
else
rest( :post, :order, nil, body )
end
end
private
def rest( verb, method , id = nil, body = nil)
id = "/#{id}" if id
options = { headers: HEADERS }
options[:body] = JSON.dump( body ) if body
HTTParty.send( verb, BASE_URL + @urls[method] + (id || "") , options )
end
def make_urls
@urls = {}
@urls[:orders] = "/venues/#{@venues}/stocks/#{@ticker}"
@urls[:quote] = @urls[:orders] + "/quote"
@urls[:order] = @urls[:orders] + "/orders"
@urls[:cancel] = @urls[:order]
end
end
# -------------------------------------------------------------------------------------------
## Strategy
UNDERCUT = 80
DESIRED_DISCOUNT_MULTIPLIER = 0.96 # note that this can cause a standoff because if the asking price doesn't dip to meet it nothing gets done.
LOW_MARKET_MIN_BUY_SIZE = 400
LOW_MARKET_MAX_BUY_SIZE = 900
BUY_NEAR_ASK_PRICE_FREQUENCY = 15
BUY_NEAR_ASK_QTY = 3_000
SLEEP_BETWEEN_BUYS = 5
api = SFApi.new( ACCOUNT, VENUE, STOCK )
pos = Position.new( api: api )
until pos.shares_remaining <= 0 do
@i ||= 0
pos.learn :quote
pos.get_open_order_statuses
pos.cancel_expired_orders
# open a small order at an optimistic position
if api.quote["ask"] && ( @i % BUY_NEAR_ASK_PRICE_FREQUENCY == BUY_NEAR_ASK_PRICE_FREQUENCY - 1 )
buy_price = api.quote["ask"] - UNDERCUT
buy_size = BUY_NEAR_ASK_QTY
# or periodically make a big disruptive order near ask price to take advantage of stable market.
elsif api.quote["last"]
low_high_range = LOW_MARKET_MAX_BUY_SIZE - LOW_MARKET_MIN_BUY_SIZE
buy_size = ( LOW_MARKET_MIN_BUY_SIZE + rand( low_high_range ) ).to_i
buy_price = ( api.quote["last"] * DESIRED_DISCOUNT_MULTIPLIER ).to_i
end
pos.act( "buy", buy_size, buy_price ) if buy_size && buy_price
sleep( SLEEP_BETWEEN_BUYS )
@i += 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment