Last active
February 25, 2016 13:58
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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