├── .assets └── ArkeStructure.jpg ├── .drone.yml ├── .gitignore ├── .rspec ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── VERSION ├── bin └── arke ├── config └── strategy.yaml ├── lib ├── arke.rb └── arke │ ├── action.rb │ ├── command.rb │ ├── command │ ├── console.rb │ ├── root.rb │ ├── start.rb │ └── version.rb │ ├── configuration.rb │ ├── exchange.rb │ ├── exchange │ ├── base.rb │ ├── binance.rb │ ├── bitfaker.rb │ ├── bitfinex.rb │ └── rubykube.rb │ ├── log.rb │ ├── open_orders.rb │ ├── order.rb │ ├── orderbook.rb │ ├── reactor.rb │ ├── strategy.rb │ └── strategy │ ├── base.rb │ └── copy.rb └── spec ├── arke ├── action_spec.rb ├── command_spec.rb ├── exchange │ ├── binance_spec.rb │ ├── bitfinex_spec.rb │ └── rubykube_spec.rb ├── open_orders_spec.rb ├── orderbook_spec.rb └── reactor_spec.rb ├── spec_helper.rb └── support ├── fixtures ├── bitfinex.yaml └── test_config.yaml └── mocked_context.rb /.assets/ArkeStructure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubykube/arke/dd54b791adb663a58174e68657cbc0ad688ff1f4/.assets/ArkeStructure.jpg -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: "master" 4 | 5 | steps: 6 | - name: test 7 | image: ruby:2.6.1 8 | commands: 9 | - gem update bundler 10 | - bundle install 11 | - bundle exec rspec 12 | 13 | - name: "Bump and tag" 14 | image: ruby:2.6.1 15 | environment: 16 | BOT_USERNAME: kite-bot 17 | BOT_NAME: Kite Bot 18 | BOT_EMAIL: kite-bot@heliostech.fr 19 | GITHUB_API_KEY: 20 | from_secret: kite_bot_key 21 | commands: 22 | - gem install bump 23 | - git config --global user.name " Kite Bot" 24 | - git config --global user.email "kite-bot@heliostech.fr" 25 | - git remote add authenticated-origin https://kite-bot:$GITHUB_API_KEY@github.com/${DRONE_REPO} 26 | - git fetch authenticated-origin 27 | - bump patch --commit-message 'Bump [ci skip]' 28 | - git tag $(cat VERSION) 29 | - git push authenticated-origin master 30 | - git push --tags authenticated-origin 31 | - git describe --tags $(git rev-list --tags --max-count=1) > .tags 32 | when: 33 | event: 34 | - push 35 | branch: 36 | - master 37 | 38 | - name: "Docker build and push" 39 | image: plugins/docker 40 | settings: 41 | username: 42 | from_secret: docker_username 43 | password: 44 | from_secret: docker_password 45 | repo: rubykube/arke 46 | when: 47 | event: 48 | - push 49 | branch: 50 | - master 51 | 52 | - name: "Notify in slack" 53 | image: plugins/slack 54 | settings: 55 | webhook: 56 | from_secret: slack_webhook 57 | channel: 58 | from_secret: slack_channel 59 | template: > 60 | {{#success build.status}} 61 | [SUCCESS] Arke {{ build.branch }} branch build by {{ build.author }} has succeeded! :sunglasses: 62 | {{else}} 63 | [FAILURE] Branch {{ build.branch }} by {{ build.author }} has failed! :pepe: 64 | {{/success}} 65 | Check the build info here: {{ build.link }} 66 | when: 67 | status: [success, failure] 68 | 69 | trigger: 70 | event: 71 | - push 72 | 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | target 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6.1 2 | 3 | MAINTAINER aartemiev@heliostech.fr 4 | 5 | ENV APP_HOME=/home/app 6 | 7 | ARG UID=1000 8 | ARG GID=1000 9 | 10 | RUN groupadd -r --gid ${GID} app \ 11 | && useradd --system --create-home --home ${APP_HOME} --shell /sbin/nologin --no-log-init \ 12 | --gid ${GID} --uid ${UID} app 13 | 14 | USER app 15 | WORKDIR $APP_HOME 16 | 17 | COPY --chown=app:app . . 18 | 19 | RUN gem update bundler && bundle install 20 | 21 | ENTRYPOINT ["bin/arke"] 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | ruby '2.6.1' 8 | 9 | gem 'clamp' 10 | gem 'em-synchrony' 11 | gem 'em-http-request' 12 | gem 'faraday' 13 | gem 'faraday_middleware' 14 | gem 'faye-websocket' 15 | gem 'json' 16 | gem 'rbtree' 17 | gem 'tty-table' 18 | 19 | gem 'pry' 20 | gem 'rspec' 21 | gem 'webmock' 22 | gem 'simplecov' 23 | gem 'simplecov-rcov' 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.6.0) 5 | public_suffix (>= 2.0.2, < 4.0) 6 | clamp (1.3.0) 7 | coderay (1.1.2) 8 | cookiejar (0.3.3) 9 | crack (0.4.3) 10 | safe_yaml (~> 1.0.0) 11 | diff-lcs (1.3) 12 | docile (1.3.1) 13 | em-http-request (1.1.5) 14 | addressable (>= 2.3.4) 15 | cookiejar (!= 0.3.1) 16 | em-socksify (>= 0.3) 17 | eventmachine (>= 1.0.3) 18 | http_parser.rb (>= 0.6.0) 19 | em-socksify (0.3.2) 20 | eventmachine (>= 1.0.0.beta.4) 21 | em-synchrony (1.0.6) 22 | eventmachine (>= 1.0.0.beta.1) 23 | equatable (0.5.0) 24 | eventmachine (1.2.7) 25 | faraday (0.15.4) 26 | multipart-post (>= 1.2, < 3) 27 | faraday_middleware (0.13.1) 28 | faraday (>= 0.7.4, < 1.0) 29 | faye-websocket (0.10.7) 30 | eventmachine (>= 0.12.0) 31 | websocket-driver (>= 0.5.1) 32 | hashdiff (0.3.8) 33 | http_parser.rb (0.6.0) 34 | json (2.1.0) 35 | method_source (0.9.2) 36 | multipart-post (2.0.0) 37 | necromancer (0.4.0) 38 | pastel (0.7.2) 39 | equatable (~> 0.5.0) 40 | tty-color (~> 0.4.0) 41 | pry (0.12.2) 42 | coderay (~> 1.1.0) 43 | method_source (~> 0.9.0) 44 | public_suffix (3.0.3) 45 | rbtree (0.4.2) 46 | rspec (3.8.0) 47 | rspec-core (~> 3.8.0) 48 | rspec-expectations (~> 3.8.0) 49 | rspec-mocks (~> 3.8.0) 50 | rspec-core (3.8.0) 51 | rspec-support (~> 3.8.0) 52 | rspec-expectations (3.8.2) 53 | diff-lcs (>= 1.2.0, < 2.0) 54 | rspec-support (~> 3.8.0) 55 | rspec-mocks (3.8.0) 56 | diff-lcs (>= 1.2.0, < 2.0) 57 | rspec-support (~> 3.8.0) 58 | rspec-support (3.8.0) 59 | safe_yaml (1.0.4) 60 | simplecov (0.16.1) 61 | docile (~> 1.1) 62 | json (>= 1.8, < 3) 63 | simplecov-html (~> 0.10.0) 64 | simplecov-html (0.10.2) 65 | simplecov-rcov (0.2.3) 66 | simplecov (>= 0.4.1) 67 | strings (0.1.4) 68 | strings-ansi (~> 0.1.0) 69 | unicode-display_width (~> 1.4.0) 70 | unicode_utils (~> 1.4.0) 71 | strings-ansi (0.1.0) 72 | tty-color (0.4.3) 73 | tty-screen (0.6.5) 74 | tty-table (0.10.0) 75 | equatable (~> 0.5.0) 76 | necromancer (~> 0.4.0) 77 | pastel (~> 0.7.2) 78 | strings (~> 0.1.0) 79 | tty-screen (~> 0.6.4) 80 | unicode-display_width (1.4.1) 81 | unicode_utils (1.4.0) 82 | webmock (3.5.1) 83 | addressable (>= 2.3.6) 84 | crack (>= 0.3.2) 85 | hashdiff 86 | websocket-driver (0.7.0) 87 | websocket-extensions (>= 0.1.0) 88 | websocket-extensions (0.1.3) 89 | 90 | PLATFORMS 91 | ruby 92 | 93 | DEPENDENCIES 94 | clamp 95 | em-http-request 96 | em-synchrony 97 | faraday 98 | faraday_middleware 99 | faye-websocket 100 | json 101 | pry 102 | rbtree 103 | rspec 104 | simplecov 105 | simplecov-rcov 106 | tty-table 107 | webmock 108 | 109 | RUBY VERSION 110 | ruby 2.6.1p33 111 | 112 | BUNDLED WITH 113 | 2.0.1 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://ci.microkube.com/api/badges/rubykube/arke/status.svg)](https://ci.microkube.com/rubykube/arke) 2 | 3 | # Arke 4 | 5 | Arke is a trading bot platform built by Openware [Cryptocurrency exchange software](https://www.openware.com). 6 | 7 | ## Development 8 | 9 | ### Setup 10 | 11 | To start local development: 12 | 13 | 1. Clone the repo: 14 | ```shell 15 | git clone git@github.com:rubykube/arke.git` 16 | ``` 17 | 2. Install dependencies 18 | ```shell 19 | bundle install 20 | ``` 21 | 22 | Now you can run Arke using `bin/arke` command. 23 | 24 | ### Example usage 25 | 26 | Arke is a liquidity aggregation tool which supports copy strategy 27 | 28 | ![ArkeStructure](.assets/ArkeStructure.jpg)Add platform host and credentials to `config/strategy.yaml` 29 | 30 | ```yaml 31 | strategy: 32 | type: 'copy' 33 | market: 'ETHUSD' 34 | target: 35 | driver: rubykube 36 | host: "http://www.example1.com" 37 | name: John 38 | key: "xxxxxxxxxx" 39 | secret: "xxxxxxxxxx" 40 | sources: 41 | - driver: source1 42 | host: "http://www.example2.com" 43 | name: Joe 44 | key: "xxxxxxxxxxx" 45 | secret: "xxxxxxxxxxxx" 46 | - driver: source2 47 | host: "http://www.example2.com" 48 | name: Joe 49 | key: "xxxxxxxxxxx" 50 | secret: "xxxxxxxxxxxx" 51 | ``` 52 | 53 | To open development console, use `bin/arke console` 54 | 55 | Now your configuration variables can be reached with 56 | ```ruby 57 | Arke::Configuration.get(:variable_name) 58 | # or 59 | Arke::Configuration.require!(:variable_name) 60 | 61 | # For example, to get target host: 62 | Arke::Configuration.require!(:target)['host'] 63 | 64 | #For api key: 65 | Arke::Configuration.require!(:target)['key'] 66 | Arke::Configuration.require!(:target)['secret'] 67 | ``` 68 | 69 | To start trading bot type 70 | 71 | ```shell 72 | bin/arke start 73 | ``` 74 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.10 2 | -------------------------------------------------------------------------------- /bin/arke: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << File.expand_path('../lib', __dir__) 4 | $LOAD_PATH << File.expand_path('../lib/arke', __dir__) 5 | 6 | require 'command' 7 | 8 | Arke::Command.run! 9 | -------------------------------------------------------------------------------- /config/strategy.yaml: -------------------------------------------------------------------------------- 1 | strategy: 2 | type: copy 3 | volume_ratio: 0.1 4 | target: 5 | driver: rubykube 6 | market: 'FTHUSD' 7 | host: "http://www.devkube.com" 8 | key: "" 9 | secret: "" 10 | rate_limit: 1 11 | sources: 12 | - driver: bitfaker 13 | market: 'tETHUSD' 14 | host: "api.bitfinex.com" 15 | key: "" 16 | secret: "" 17 | rate_limit: 0.2 18 | - driver: bitfinex 19 | market: 'tETHUSD' 20 | host: "api.bitfinex.com" 21 | key: "" 22 | secret: "" 23 | rate_limit: 0.2 24 | - driver: binance 25 | market: 'ETHUSDT' 26 | host: "api.binance.com" 27 | key: "" 28 | secret: "" 29 | rate_limit: 0.2 30 | -------------------------------------------------------------------------------- /lib/arke.rb: -------------------------------------------------------------------------------- 1 | require 'configuration' 2 | require 'log' 3 | require 'reactor' 4 | require 'exchange' 5 | require 'strategy' 6 | 7 | module Arke 8 | end 9 | -------------------------------------------------------------------------------- /lib/arke/action.rb: -------------------------------------------------------------------------------- 1 | module Arke 2 | # This class represents Actions as jobs which are executed by Exchanges 3 | class Action 4 | attr_reader :type, :params, :destination 5 | 6 | # Takes type of action and params: 7 | # :shutdown:: +params+ - nil 8 | # :create_order:: +params+ - order 9 | # :cancel_order:: +params+ - order 10 | def initialize(type, destination, params=nil) 11 | @type = type 12 | @params = params 13 | @destination = destination 14 | end 15 | 16 | def to_s 17 | "#Type: #{@type}, params: #{@params}" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/arke/command.rb: -------------------------------------------------------------------------------- 1 | require 'clamp' 2 | require 'yaml' 3 | 4 | require 'command/root' 5 | require 'arke' 6 | 7 | module Arke 8 | module Command 9 | def run! 10 | load_configuration 11 | Arke::Log.define 12 | Root.run 13 | end 14 | module_function :run! 15 | 16 | def load_configuration 17 | config = YAML.load_file('config/strategy.yaml') 18 | 19 | Arke::Configuration.define { |c| c.strategy = config['strategy'] } 20 | end 21 | module_function :load_configuration 22 | 23 | # NOTE: we can add more features here (colored output, etc.) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/arke/command/console.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | 3 | module Arke 4 | module Command 5 | class Console < Clamp::Command 6 | 7 | def execute 8 | Pry.hooks.add_hook(:before_session, 'arke_load') do |output, binding, pry| 9 | output.puts "Arke development console" 10 | end 11 | 12 | # binding.pry 13 | Pry.config.prompt_name = 'arke' 14 | Pry.config.requires = ['openssl'] 15 | Pry.start 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/arke/command/root.rb: -------------------------------------------------------------------------------- 1 | require 'command/start' 2 | require 'command/console' 3 | require 'command/version' 4 | 5 | module Arke 6 | module Command 7 | class Root < Clamp::Command 8 | subcommand 'start', 'Starting the bot', Start 9 | subcommand 'console', 'Start a development console', Console 10 | subcommand 'version', 'Print the Arke version', Version 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/arke/command/start.rb: -------------------------------------------------------------------------------- 1 | module Arke 2 | module Command 3 | class Start < Clamp::Command 4 | 5 | option "--dry", :flag, "dry run on the target" 6 | 7 | def execute 8 | config = Arke::Configuration.require!(:strategy) 9 | config['dry'] = dry? 10 | config['target']['driver'] = 'bitfaker' if dry? 11 | reactor = Arke::Reactor.new(config) 12 | reactor.run 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/arke/command/version.rb: -------------------------------------------------------------------------------- 1 | module Arke 2 | module Command 3 | class Version < Clamp::Command 4 | def execute 5 | puts "Arke version #{read_version}" 6 | end 7 | 8 | def read_version 9 | File.read(File.expand_path('../../../VERSION', __dir__)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/arke/configuration.rb: -------------------------------------------------------------------------------- 1 | module Arke 2 | # This class stores configuration for Arke application 3 | module Configuration 4 | PropertyNotSetError = Class.new(StandardError) 5 | 6 | class << self 7 | # Inits configuration holder 8 | def define 9 | @config ||= OpenStruct.new 10 | yield(@config) 11 | end 12 | 13 | # Returns requested key 14 | # * returns +nil+ if key is not set 15 | def get(key) 16 | @config ||= OpenStruct.new 17 | @config[key] 18 | end 19 | 20 | # Returns requested key 21 | # * raises PropertyNotSetError if key is not set 22 | def require!(key) 23 | get(key) || (raise PropertyNotSetError.new) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/arke/exchange.rb: -------------------------------------------------------------------------------- 1 | require 'exchange/base' 2 | require 'exchange/bitfinex' 3 | require 'exchange/rubykube' 4 | require 'exchange/bitfaker' 5 | require 'exchange/binance' 6 | 7 | module Arke 8 | # Exchange module, contains Exchanges drivers implementation 9 | module Exchange 10 | # Fabric method, creates proper Exchange instance 11 | # * takes +strategy+ (+Arke::Strategy+) and passes to Exchange initializer 12 | # * takes +config+ (hash) and passes to Exchange initializer 13 | # * takes +config+ and resolves correct Exchange class with +exchange_class+ helper 14 | def self.create(config) 15 | exchange_class(config['driver']).new(config) 16 | end 17 | 18 | # Takes +dirver+ - +String+ 19 | # Resolves correct Exchange class by it's name 20 | def self.exchange_class(driver) 21 | Arke::Exchange.const_get(driver.capitalize) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/arke/exchange/base.rb: -------------------------------------------------------------------------------- 1 | module Arke::Exchange 2 | 3 | # Base class for all exchanges 4 | class Base 5 | attr_reader :queue, :min_delay, :open_orders, :market 6 | attr_accessor :timer 7 | 8 | def initialize(opts) 9 | @market = opts['market'] 10 | @driver = opts['driver'] 11 | @api_key = opts['key'] 12 | @secret = opts['secret'] 13 | @queue = EM::Queue.new 14 | @timer = nil 15 | 16 | rate_limit = opts['rate_limit'] || 1.0 17 | rate_limit = 1.0 if rate_limit <= 0 18 | @min_delay = 1.0 / rate_limit 19 | 20 | @open_orders = Arke::OpenOrders.new(@market) 21 | end 22 | 23 | def start; end 24 | 25 | def print 26 | return unless @orderbook 27 | puts "Exchange #{@driver} market: #{@market}" 28 | puts @orderbook.print(:buy) 29 | puts @orderbook.print(:sell) 30 | end 31 | 32 | def build_error(response) 33 | JSON.parse(response.body) 34 | rescue StandardError => e 35 | "Code: #{response.env.status} Message: #{response.env.reason_phrase}" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/arke/exchange/binance.rb: -------------------------------------------------------------------------------- 1 | require 'orderbook' 2 | require 'json' 3 | require 'openssl' 4 | 5 | module Arke::Exchange 6 | class Binance < Base 7 | attr_reader :last_update_id 8 | attr_accessor :orderbook 9 | 10 | def initialize(opts) 11 | super 12 | 13 | @url = "wss://stream.binance.com:9443/ws/#{@market}@depth" 14 | @connection = Faraday.new("https://www.binance.com") do |builder| 15 | builder.adapter :em_synchrony 16 | end 17 | 18 | @orderbook = Arke::Orderbook.new(@market) 19 | @last_update_id = 0 20 | @rest_api_connection = Faraday.new("https://#{opts['host']}") do |builder| 21 | builder.adapter :em_synchrony 22 | builder.headers['Content-Type'] = 'application/x-www-form-urlencoded' 23 | end 24 | end 25 | 26 | # TODO: remove EM (was used only for debug) 27 | def start 28 | get_snapshot 29 | 30 | @ws = Faye::WebSocket::Client.new(@url) 31 | 32 | @ws.on(:message) do |e| 33 | on_message(e) 34 | end 35 | end 36 | 37 | def on_message(mes) 38 | data = JSON.parse(mes.data) 39 | return if @last_update_id >= data['U'] 40 | @last_update_id = data['u'] 41 | process(data['b'], :buy) unless data['b'].empty? 42 | process(data['a'], :sell) unless data['a'].empty? 43 | end 44 | 45 | def process(data, side) 46 | data.each do |order| 47 | if order[1].to_f.zero? 48 | @orderbook.delete(build_order(order, side)) 49 | next 50 | end 51 | 52 | orderbook.update( 53 | build_order(order, side) 54 | ) 55 | end 56 | end 57 | 58 | def build_order(data, side) 59 | Arke::Order.new( 60 | @market, 61 | data[0].to_f, 62 | data[1].to_f, 63 | side 64 | ) 65 | end 66 | 67 | def get_snapshot 68 | snapshot = JSON.parse(@connection.get("api/v1/depth?symbol=#{@market.upcase}&limit=1000").body) 69 | @last_update_id = snapshot['lastUpdateId'] 70 | process(snapshot['bids'], :buy) 71 | process(snapshot['asks'], :sell) 72 | end 73 | 74 | def create_order(order) 75 | timestamp = Time.now.to_i * 1000 76 | body = { 77 | symbol: @market.upcase, 78 | side: order.side.upcase, 79 | type: 'LIMIT', 80 | timeInForce: 'GTC', 81 | quantity: order.amount.to_f, 82 | price: order.price.to_f, 83 | recvWindow: '5000', 84 | timestamp: timestamp 85 | } 86 | 87 | post('api/v3/order', body) 88 | end 89 | 90 | def generate_signature(data, timestamp) 91 | query = "" 92 | data.each { |key, value| query << "#{key}=#{value}&" } 93 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @secret, query.chomp('&')) 94 | end 95 | 96 | private 97 | 98 | def post(path, params = nil) 99 | request = @rest_api_connection.post(path) do |req| 100 | req.headers['X-MBX-APIKEY'] = @api_key 101 | req.body = URI.encode_www_form(generate_body(params)) 102 | end 103 | 104 | Arke::Log.fatal(build_error(request)) if request.env.status != 200 105 | request 106 | end 107 | 108 | def generate_body(data) 109 | query = "" 110 | data.each { |key, value| query << "#{key}=#{value}&" } 111 | sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @secret, query.chomp('&')) 112 | data.merge(signature: sig) 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/arke/exchange/bitfaker.rb: -------------------------------------------------------------------------------- 1 | require 'exchange/base' 2 | require 'orderbook' 3 | 4 | module Arke::Exchange 5 | class Bitfaker < Base 6 | 7 | attr_reader :orderbook 8 | 9 | def initialize(opts) 10 | super 11 | 12 | @orderbook = Arke::Orderbook.new(@market) 13 | end 14 | 15 | def start 16 | load_orderbook 17 | end 18 | 19 | def create_order(order) 20 | pp order 21 | end 22 | 23 | def stop_order(order) 24 | pp order 25 | end 26 | 27 | def ping; end 28 | 29 | private 30 | 31 | def load_orderbook 32 | fixture = YAML.load_file('spec/support/fixtures/bitfinex.yaml') 33 | orders = fixture[1] 34 | orders.each { |order| add_order(order) } 35 | end 36 | 37 | def add_order(order) 38 | _id, price, amount = order 39 | side = (amount.negative?) ? :sell : :buy 40 | amount = amount.abs 41 | @orderbook.update(Arke::Order.new(@market, price, amount, side)) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/arke/exchange/bitfinex.rb: -------------------------------------------------------------------------------- 1 | require 'faye/websocket' 2 | require 'eventmachine' 3 | require 'json' 4 | require 'exchange/base' 5 | require 'orderbook' 6 | 7 | module Arke::Exchange 8 | class Bitfinex < Base 9 | 10 | attr_reader :orderbook 11 | 12 | def initialize(opts) 13 | super 14 | 15 | @url = 'wss://%s/ws/2' % opts['host'] 16 | @orderbook = Arke::Orderbook.new(@market) 17 | end 18 | 19 | def process_message(msg) 20 | if msg.kind_of?(Array) 21 | process_channel_message(msg) 22 | elsif msg.kind_of?(Hash) 23 | process_event_message(msg) 24 | end 25 | end 26 | 27 | def process_channel_message(msg) 28 | data = msg[1] 29 | 30 | if data.length == 3 31 | process_data(data) 32 | elsif data.length > 3 33 | data.each { |order| process_data(order) } 34 | end 35 | end 36 | 37 | def process_data(data) 38 | order = build_order(data) 39 | if data[1].zero? 40 | @orderbook.delete(order) 41 | else 42 | @orderbook.update(order) 43 | end 44 | end 45 | 46 | def build_order(data) 47 | price, _count, amount = data 48 | side = :buy 49 | if amount.negative? 50 | side = :sell 51 | amount = amount * -1 52 | end 53 | Arke::Order.new(@market, price, amount, side) 54 | end 55 | 56 | def process_event_message(msg) 57 | case msg['event'] 58 | when 'auth' 59 | when 'subscribed' 60 | Arke::Log.info "Event: #{msg['event']}" 61 | when 'unsubscribed' 62 | when 'info' 63 | when 'conf' 64 | when 'error' 65 | Arke::Log.info "Event: #{msg['event']} ignored" 66 | end 67 | end 68 | 69 | def on_open(e) 70 | sub = { 71 | event: "subscribe", 72 | channel: "book", 73 | symbol: @market, 74 | prec: "P0", 75 | freq: "F0", 76 | } 77 | 78 | Arke::Log.info 'Open event' + sub.to_s 79 | EM.next_tick { 80 | @ws.send(JSON.generate(sub)) 81 | } 82 | end 83 | 84 | def on_message(e) 85 | msg = JSON.parse(e.data) 86 | process_message(msg) 87 | end 88 | 89 | def on_close(e) 90 | Arke::Log.info "Closing code: #{e.code} Reason: #{e.reason}" 91 | end 92 | 93 | def start 94 | @ws = Faye::WebSocket::Client.new(@url) 95 | 96 | @ws.on(:open) do |e| 97 | on_open(e) 98 | end 99 | 100 | @ws.on(:message) do |e| 101 | on_message(e) 102 | end 103 | 104 | @ws.on(:close) do |e| 105 | on_close(e) 106 | end 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/arke/exchange/rubykube.rb: -------------------------------------------------------------------------------- 1 | require 'exchange/base' 2 | require 'open_orders' 3 | 4 | module Arke::Exchange 5 | # This class holds Rubykube Exchange logic implementation 6 | class Rubykube < Base 7 | 8 | # Takes config (hash), strategy(+Arke::Strategy+ instance) 9 | # * +strategy+ is setted in +super+ 10 | # * creates @connection for RestApi 11 | def initialize(config) 12 | super 13 | 14 | @connection = Faraday.new("#{config['host']}/api/v2") do |builder| 15 | # builder.response :logger 16 | builder.response :json 17 | builder.adapter :em_synchrony 18 | end 19 | end 20 | 21 | # Ping the api 22 | def ping 23 | @connection.get '/barong/identity/ping' 24 | end 25 | 26 | # Takes +order+ (+Arke::Order+ instance) 27 | # * creates +order+ via RestApi 28 | def create_order(order) 29 | response = post( 30 | 'peatio/market/orders', 31 | { 32 | market: order.market.downcase, 33 | side: order.side.to_s, 34 | volume: order.amount, 35 | price: order.price 36 | } 37 | ) 38 | @open_orders.add_order(order, response.env.body['id']) if response.env.status == 201 && response.env.body['id'] 39 | 40 | response 41 | end 42 | 43 | # Takes +order+ (+Arke::Order+ instance) 44 | # * cancels +order+ via RestApi 45 | def stop_order(id) 46 | response = post( 47 | "peatio/market/orders/#{id}/cancel" 48 | ) 49 | @open_orders.remove_order(id) 50 | 51 | response 52 | end 53 | 54 | private 55 | 56 | # Helper method to perform post requests 57 | # * takes +conn+ - faraday connection 58 | # * takes +path+ - request url 59 | # * takes +params+ - body for +POST+ request 60 | def post(path, params = nil) 61 | response = @connection.post do |req| 62 | req.headers = generate_headers 63 | req.url path 64 | req.body = params.to_json 65 | end 66 | Arke::Log.fatal(build_error(response)) if response.env.status != 201 67 | response 68 | end 69 | 70 | # Helper method, generates headers to authenticate with +api_key+ 71 | def generate_headers 72 | nonce = Time.now.to_i.to_s 73 | { 74 | 'X-Auth-Apikey' => @api_key, 75 | 'X-Auth-Nonce' => nonce, 76 | 'X-Auth-Signature' => OpenSSL::HMAC.hexdigest('SHA256', @secret, nonce + @api_key), 77 | 'Content-Type' => 'application/json' 78 | } 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/arke/log.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Arke 4 | # Holds Arke apllication logger 5 | module Log 6 | class << self 7 | # Inits logger 8 | def define 9 | @logger ||= Logger.new($stderr) 10 | end 11 | 12 | # Logs +INFO+ message 13 | def info(message) 14 | @logger.info message 15 | end 16 | 17 | # Logs +FATAL+ message 18 | def fatal(message) 19 | @logger.fatal message 20 | end 21 | 22 | # Logs +DEBUG+ message 23 | def debug(message) 24 | @logger.debug message 25 | end 26 | 27 | # Logs +ERROR+ message 28 | def error(message) 29 | @logger.error message 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/arke/open_orders.rb: -------------------------------------------------------------------------------- 1 | require 'order' 2 | 3 | module Arke 4 | class OpenOrders 5 | def initialize(market) 6 | @market = market 7 | @book = { 8 | buy: {}, 9 | sell: {} 10 | } 11 | end 12 | 13 | def price_level(side, price) 14 | @book[side][price] 15 | end 16 | 17 | def contains?(side, price) 18 | !(@book[side][price].nil? || @book[side][price].empty?) 19 | end 20 | 21 | def price_amount(side, price) 22 | @book[side][price].sum { |_id, order| order.amount } 23 | end 24 | 25 | def add_order(order, id) 26 | @book[order.side][order.price] ||= {} 27 | @book[order.side][order.price][id] = order 28 | end 29 | 30 | def remove_order(id) 31 | @book[:sell].each { |k , v| v.delete(id)} 32 | @book[:buy].each { |k, v| v.delete(id)} 33 | end 34 | 35 | def get_diff(orderbook) 36 | diff = { 37 | create: { buy: [], sell: [] }, 38 | delete: { buy: [], sell: [] }, 39 | update: { buy: [], sell: [] } 40 | } 41 | 42 | [:buy, :sell].each do |side| 43 | our = @book[side] 44 | their = orderbook.book[side] 45 | 46 | their.each do |price, amount| 47 | if !contains?(side, price) 48 | diff[:create][side].push(Arke::Order.new(@market, price, amount, side)) 49 | else 50 | our_amount = price_amount(side, price) 51 | # creating additioanl order to adjust volume 52 | if our_amount != amount 53 | diff[:update][side].push(Arke::Order.new(@market, price, amount - our_amount, side)) 54 | end 55 | end 56 | end 57 | 58 | our.each do |_price, hash| 59 | hash.each do |id, order| 60 | diff[:delete][side].push(id) unless orderbook.contains?(order) 61 | end 62 | end 63 | end 64 | 65 | diff 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/arke/order.rb: -------------------------------------------------------------------------------- 1 | module Arke 2 | class Order 3 | 4 | attr_reader :market, :price, :amount, :side 5 | 6 | def initialize(market, price, amount, side) 7 | @market = market 8 | @price = price 9 | @amount = amount 10 | @side = side 11 | end 12 | 13 | def to_s 14 | "#{@market}: #{@side} #{@price} x #{@amount}" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/arke/orderbook.rb: -------------------------------------------------------------------------------- 1 | require 'rbtree' 2 | require 'tty-table' 3 | require 'order' 4 | 5 | module Arke 6 | class Orderbook 7 | 8 | attr_reader :book, :market 9 | 10 | def initialize(market) 11 | @market = market 12 | @book = { 13 | index: ::RBTree.new, 14 | buy: ::RBTree.new, 15 | sell: ::RBTree.new 16 | } 17 | @book[:buy].readjust { |a, b| b <=> a } 18 | end 19 | 20 | def clone 21 | ob = Orderbook.new(@market) 22 | ob.merge!(self) 23 | 24 | ob 25 | end 26 | 27 | def update(order) 28 | @book[order.side][order.price] = order.amount 29 | end 30 | 31 | def delete(order) 32 | @book[order.side].delete(order.price) 33 | end 34 | 35 | def contains?(order) 36 | !@book[order.side][order.price].nil? 37 | end 38 | 39 | # get with the lowest price 40 | def get(side) 41 | @book[side].first 42 | end 43 | 44 | def print(side = :buy) 45 | header = ['Price', 'Amount'] 46 | rows = [] 47 | @book[side].each do |price, amount| 48 | rows << ['%.6f' % price, '%.6f' % amount] 49 | end 50 | table = TTY::Table.new header, rows 51 | table.render(:ascii, padding: [0, 2], alignment: [:right]) 52 | end 53 | 54 | def [](side) 55 | @book[side] 56 | end 57 | 58 | def merge!(ob) 59 | @book.each do |k, _v| 60 | @book[k].merge!(ob[k]) { |_price, amount, ob_amount| amount + ob_amount } 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/arke/reactor.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday_middleware' 3 | require 'em-synchrony' 4 | require 'em-synchrony/em-http' 5 | 6 | require 'exchange' 7 | require 'strategy' 8 | 9 | module Arke 10 | # Main event ractor loop 11 | class Reactor 12 | 13 | # * @shutdown is a flag which controls strategy execution 14 | def initialize(config) 15 | @shutdown = false 16 | @strategy = Arke::Strategy.create(config) 17 | @dax = build_dax(config) 18 | 19 | trap('INT') { stop } 20 | end 21 | 22 | def build_dax(config) 23 | dax = {} 24 | config['sources'].each { |ex| 25 | dax[ex['driver'].to_sym] = Arke::Exchange.create(ex) 26 | } 27 | 28 | dax[:target] = Arke::Exchange.create(config['target']) 29 | 30 | return dax 31 | end 32 | 33 | # * traps SIGINT 34 | # * strategy execution rate is limited by target's +rate_limit+ 35 | def run 36 | strategy_delay = @dax.collect { |_k, v| v.min_delay }.min 37 | 38 | EM.synchrony do 39 | @dax.each do |name, exchange| 40 | Arke::Log.debug "Starting Exchange: #{name}" 41 | 42 | exchange.timer = EM::Synchrony::add_periodic_timer(exchange.min_delay) do 43 | exchange.queue.pop do |action| 44 | Arke::Log.debug "Scheduling Action #{Time.now} - Exchange #{name} Delay #{exchange.min_delay} - Queue size: #{exchange.queue.size}" 45 | Arke::Log.debug "pop: #{action}" 46 | schedule(action) 47 | end 48 | end 49 | 50 | exchange.start 51 | end 52 | 53 | # order stacking is a very big issue here, I multiply by 2 because I yield 2 orders 54 | # one for buy and one for sell 55 | @timer = EM::Synchrony::add_periodic_timer(strategy_delay) do 56 | execute_strategy if queues_empty? 57 | end 58 | end 59 | end 60 | 61 | def queues_empty? 62 | queue_sizes = @dax.collect { |_k, v| v.queue.size } 63 | queue_sizes.max.zero? 64 | end 65 | 66 | def execute_strategy 67 | Arke::Log.debug "Calling Strategy #{Time.now}" 68 | @strategy.call(@dax) do |action| 69 | @dax[action.destination].queue.push(action) 70 | end 71 | end 72 | 73 | def schedule(action) 74 | case action.type 75 | when :ping 76 | @target.ping 77 | when :order_create 78 | @dax[action.destination].create_order(action.params[:order]) 79 | when :order_stop 80 | @dax[action.destination].stop_order(action.params[:id]) 81 | else 82 | Arke::Log.error "Unknown Action type: #{action.type}" 83 | end 84 | end 85 | 86 | # Stops workers and strategy execution 87 | # * sets @shutdown flag to +true+ 88 | # * broadcasts +:shutdown+ action to workers 89 | def stop 90 | puts 'Shutdown trading' 91 | @shutdown = true 92 | @timer.cancel 93 | @dax.each { |_name, exchange| exchange.timer.cancel } 94 | EM.stop 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/arke/strategy.rb: -------------------------------------------------------------------------------- 1 | require 'strategy/base' 2 | require 'strategy/copy' 3 | 4 | module Arke 5 | # Strategy module, contains Strategy types implementation 6 | module Strategy 7 | # Fabric method, creates proper Exchange instance 8 | # * takes +config+ (hash) and passes to +Strategy+ initializer 9 | def self.create(config) 10 | strategy_class(config['type']).new(config) 11 | end 12 | 13 | # Takes +type+ - +String+ 14 | # * Resolves correct Strategy class by it's type 15 | def self.strategy_class(type) 16 | Arke::Strategy.const_get(type.capitalize) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/arke/strategy/base.rb: -------------------------------------------------------------------------------- 1 | module Arke::Strategy 2 | 3 | # Base class for all strategies 4 | class Base 5 | def initialize(config) 6 | @config = config 7 | @volume_ratio = config['volume_ratio'] 8 | @spread = config['spread'] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/arke/strategy/copy.rb: -------------------------------------------------------------------------------- 1 | require 'action' 2 | 3 | module Arke::Strategy 4 | # This class implements basic copy strategy behaviour 5 | # * aggreagates orders from sources 6 | # * push order to target 7 | class Copy < Base 8 | 9 | # Processes orders and decides what action should be sent to @target 10 | def call(dax, &block) 11 | sources = dax.select { |k, _v| k != :target } 12 | ob = merge_orderbooks(sources, dax[:target].market) 13 | ob = scale_amounts(ob) 14 | 15 | open_orders = dax[:target].open_orders 16 | diff = open_orders.get_diff(ob) 17 | 18 | [:buy, :sell].each do |side| 19 | create = diff[:create][side] 20 | delete = diff[:delete][side] 21 | update = diff[:update][side] 22 | 23 | 24 | if !create.length.zero? 25 | order = create.first 26 | yield Arke::Action.new(:order_create, :target, { order: order }) 27 | elsif !delete.length.zero? 28 | yield Arke::Action.new(:order_stop, :target, { id: delete.first }) 29 | elsif !update.length.zero? 30 | order = update.first 31 | if order.amount > 0.0 32 | yield Arke::Action.new(:order_create, :target, { order: order }) 33 | else 34 | new_amount = open_orders.price_amount(side, order.price) + order.amount 35 | new_order = Arke::Order.new(order.market, order.price, new_amount, order.side) 36 | 37 | open_orders.price_level(side, order.price).each do |id, _ord| 38 | yield Arke::Action.new(:order_stop, :target, { id: id }) 39 | end 40 | 41 | yield Arke::Action.new(:order_create, :target, { order: new_order }) 42 | end 43 | end 44 | end 45 | end 46 | 47 | def scale_amounts(orderbook) 48 | ob = Arke::Orderbook.new(orderbook.market) 49 | 50 | [:buy, :sell].each do |side| 51 | orderbook[side].each do |price, amount| 52 | ob[side][price] = amount * @volume_ratio 53 | end 54 | end 55 | 56 | ob 57 | end 58 | 59 | def merge_orderbooks(sources, market) 60 | ob = Arke::Orderbook.new(market) 61 | 62 | sources.each do |_key, source| 63 | source_book = source.orderbook.clone 64 | 65 | # discarding 1st level 66 | source_book[:sell].shift 67 | source_book[:buy].shift 68 | 69 | ob.merge!(source_book) 70 | end 71 | 72 | ob 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/arke/action_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Action do 2 | let(:type) { :create_order } 3 | let(:dest) { :bitfaker } 4 | let(:params) { Arke::Order.new('ethusd', 1, 1, :buy) } 5 | 6 | it 'creates istruction' do 7 | action = Arke::Action.new(type, dest, params) 8 | 9 | expect(action.type).to eq(type) 10 | expect(action.params).to eq(params) 11 | expect(action.destination).to eq(dest) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/arke/command_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Command do 2 | let(:config) { YAML.load_file('config/strategy.yaml')['strategy'] } 3 | 4 | it 'loads configuration' do 5 | Arke::Command.load_configuration 6 | 7 | expect(Arke::Configuration.get(:strategy)).to eq(config) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/arke/exchange/binance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'arke' 2 | require 'action' 3 | require 'openssl' 4 | 5 | describe Arke::Exchange::Binance do 6 | include_context 'mocked binance' 7 | 8 | before(:all) { Arke::Log.define } 9 | 10 | let(:binance) do 11 | Arke::Exchange::Binance.new( 12 | { 13 | 'host' => 'api.binance.com', 14 | 'market' => 'ETHUSDT', 15 | 'key' => 'Uwg8wqlxueiLCsbTXjlogviL8hdd60', 16 | 'secret' => 'OwpadzSYOSkzweoJkjPrFeVgjOwOuxVHk8FXIlffdWw' 17 | } 18 | ) 19 | end 20 | 21 | context 'ojbect initialization' do 22 | it 'is a sublass of Arke::Exchange::Base' do 23 | expect(Arke::Exchange::Binance.superclass).to eq(Arke::Exchange::Base) 24 | end 25 | 26 | it 'has an orderbook' do 27 | expect(binance.orderbook).to be_an_instance_of(Arke::Orderbook) 28 | end 29 | end 30 | 31 | context 'getting snapshot' do 32 | let(:snapshot_buy_order_1) { Arke::Order.new('ETHUSDT', 135.84000000, 6.62227000, :buy) } 33 | let(:snapshot_buy_order_2) { Arke::Order.new('ETHUSDT', 135.85000000, 0.57176000, :buy) } 34 | let(:snapshot_buy_order_3) { Arke::Order.new('ETHUSDT', 135.87000000, 36.43875000, :buy) } 35 | 36 | let(:snapshot_sell_order_1) { Arke::Order.new('ETHUSDT', 135.91000000, 0.00070000, :sell) } 37 | let(:snapshot_sell_order_2) { Arke::Order.new('ETHUSDT', 135.93000000, 8.00000000, :sell) } 38 | let(:snapshot_sell_order_3) { Arke::Order.new('ETHUSDT', 135.95000000, 1.11699000, :sell) } 39 | 40 | it 'gets a snapshot' do 41 | binance.get_snapshot 42 | expect(binance.orderbook.book[:buy].empty?).to be false 43 | expect(binance.orderbook.book[:sell].empty?).to be false 44 | end 45 | 46 | it 'gets filled with buy orders from snapshot' do 47 | binance.get_snapshot 48 | expect(binance.orderbook.contains?(snapshot_buy_order_1)).to eq(true) 49 | expect(binance.orderbook.contains?(snapshot_buy_order_2)).to eq(true) 50 | expect(binance.orderbook.contains?(snapshot_buy_order_3)).to eq(true) 51 | end 52 | 53 | it 'gets filled with sell orders from snapshot' do 54 | binance.get_snapshot 55 | expect(binance.orderbook.contains?(snapshot_sell_order_1)).to eq(true) 56 | expect(binance.orderbook.contains?(snapshot_sell_order_2)).to eq(true) 57 | expect(binance.orderbook.contains?(snapshot_sell_order_3)).to eq(true) 58 | end 59 | end 60 | 61 | context 'binance message processing' do 62 | let(:example_message) do 63 | OpenStruct.new({ 64 | 'data' => "{\"e\":\"depthUpdate\",\"E\":1551458278012,\"s\":\"ETHUSDT\",\"U\":320977456,\"u\":320977468,\"b\":[[\"136.43000000\",\"0.66174000\",[]],[\"136.07000000\",\"29.38270000\",[]]],\"a\":[[\"136.44000000\",\"5.15285000\",[]],[\"136.45000000\",\"165.29973000\",[]],[\"136.50000000\",\"0.16122000\",[]],[\"136.51000000\",\"0.93508000\",[]],[\"136.52000000\",\"25.20000000\",[]]]}" 65 | }) 66 | end 67 | 68 | let(:example_messsage_buy_order_1) { Arke::Order.new('ETHUSDT', 136.07000000, 29.38270000, :buy) } 69 | let(:example_messsage_buy_order_2) { Arke::Order.new('ETHUSDT', 136.43000000, 0.66174000, :buy) } 70 | 71 | let(:example_messsage_sell_order_1) { Arke::Order.new('ETHUSDT', 136.44000000, 5.15285000, :sell) } 72 | let(:example_messsage_sell_order_2) { Arke::Order.new('ETHUSDT', 136.45000000, 165.29973000, :sell) } 73 | let(:example_messsage_sell_order_3) { Arke::Order.new('ETHUSDT', 136.50000000, 0.16122000, :sell) } 74 | let(:example_messsage_sell_order_4) { Arke::Order.new('ETHUSDT', 136.51000000, 0.93508000, :sell) } 75 | let(:example_messsage_sell_order_5) { Arke::Order.new('ETHUSDT', 136.52000000, 25.20000000, :sell) } 76 | 77 | it 'parses message and fills orderbook with data from it' do 78 | binance.on_message(example_message) 79 | 80 | expect(binance.orderbook.contains?(example_messsage_buy_order_1)).to eq(true) 81 | expect(binance.orderbook.contains?(example_messsage_buy_order_2)).to eq(true) 82 | 83 | expect(binance.orderbook.contains?(example_messsage_sell_order_1)).to eq(true) 84 | expect(binance.orderbook.contains?(example_messsage_sell_order_2)).to eq(true) 85 | expect(binance.orderbook.contains?(example_messsage_sell_order_3)).to eq(true) 86 | expect(binance.orderbook.contains?(example_messsage_sell_order_4)).to eq(true) 87 | expect(binance.orderbook.contains?(example_messsage_sell_order_5)).to eq(true) 88 | end 89 | 90 | context 'handling last_update_id value' do 91 | let(:with_invalid_update_value) do 92 | OpenStruct.new({ 93 | 'data' => "{\"e\":\"depthUpdate\",\"E\":1551458278013,\"s\":\"ETHUSDT\",\"U\":320977465,\"u\":320977471,\"b\":[[\"136.44000000\",\"0.66174000\",[]],[\"137.07000000\",\"29.38270000\",[]]],\"a\":[[\"136.44000000\",\"5.15285000\",[]],[\"136.45000000\",\"165.29973000\",[]],[\"136.50000000\",\"0.16122000\",[]],[\"136.51000000\",\"0.93508000\",[]],[\"136.52000000\",\"25.20000000\",[]]]}" 94 | }) 95 | end 96 | 97 | let(:with_valid_update_value) do 98 | OpenStruct.new({ 99 | 'data' => "{\"e\":\"depthUpdate\",\"E\":1551458278014,\"s\":\"ETHUSDT\",\"U\":320977469,\"u\":320977483,\"b\":[[\"136.44000000\",\"0.66174000\",[]],[\"137.07000000\",\"29.38270000\",[]]],\"a\":[[\"136.44000000\",\"5.15285000\",[]],[\"136.45000000\",\"165.29973000\",[]],[\"136.50000000\",\"0.16122000\",[]],[\"136.51000000\",\"0.93508000\",[]],[\"136.52000000\",\"25.20000000\",[]]]}" 100 | }) 101 | end 102 | 103 | it 'accepts only message with correct update_id and changes current one' do 104 | binance.on_message(example_message) 105 | expect{ binance.on_message(with_invalid_update_value) }.to_not change{ binance.last_update_id } 106 | expect{ binance.on_message(with_valid_update_value) }.to change{ binance.last_update_id } 107 | end 108 | end 109 | 110 | context 'price level removing' do 111 | let(:order_to_remove) { Arke::Order.new('ethusdt', 136.07000000, 29.38270000, :buy) } 112 | let(:price_level_to_remove) { [[136.07000000, 0.00000000,[]]] } 113 | 114 | it 'removes specified order' do 115 | binance.on_message(example_message) 116 | expect{ binance.process(price_level_to_remove, :buy) }.to change{ binance.orderbook.contains?(order_to_remove) }.from(true).to(false) 117 | end 118 | end 119 | end 120 | 121 | context 'order_create' do 122 | let(:order) { Arke::Order.new('ETHUSDT', 250, 1, :buy) } 123 | let(:timestamp) { "1551720218" } 124 | let(:query) do 125 | "symbol=#{order.market}&side=#{order.side.upcase}&type=LIMIT&timeInForce=GTC&quantity=#{order.amount.to_f}"\ 126 | "&price=#{order.price.to_f}&recvWindow=5000×tamp=#{timestamp}" 127 | end 128 | let(:query_hash) do 129 | { 130 | symbol: order.market, 131 | side: order.side.upcase, 132 | type: 'LIMIT', 133 | timeInForce: 'GTC', 134 | quantity: order.amount.to_f, 135 | price: order.price.to_f, 136 | recvWindow: '5000', 137 | timestamp: timestamp 138 | } 139 | end 140 | let(:signature) do 141 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), 'OwpadzSYOSkzweoJkjPrFeVgjOwOuxVHk8FXIlffdWw', query) 142 | end 143 | 144 | it 'creates an order on Binance' do 145 | expect(binance.create_order(order).status).to eq(200) 146 | end 147 | 148 | it 'creates proper signature' do 149 | expect(binance.generate_signature(query_hash, timestamp)).to eq(signature) 150 | end 151 | 152 | it 'gets 401 on request with wrong api key' do 153 | binance.instance_variable_set(:@api_key, SecureRandom.hex) 154 | 155 | expect { binance.create_order(order) }.to output(/Code: 401/).to_stderr_from_any_process 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/arke/exchange/bitfinex_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Exchange::Bitfinex do 2 | let(:config) { YAML.load_file('spec/support/fixtures/test_config.yaml') } 3 | let(:bitfinex_config) do 4 | { 5 | 'driver' => 'bitfinex', 6 | 'market' => 'tETHUSD', 7 | 'host' => "api.bitfinex.com", 8 | 'key' => "", 9 | 'secret' => "", 10 | 'rate_limit' => 1.0 11 | } 12 | end 13 | let(:strategy) { Arke::Strategy::Copy.new(config) } 14 | let(:bitfinex) { Arke::Exchange::Bitfinex.new(bitfinex_config) } 15 | 16 | context 'Bitfinex class' do 17 | let(:data_create) { [1, 10, 20] } 18 | let(:data_create_sell) { [2, 22, -33] } 19 | let(:data_delete) { [3, 0, 44] } 20 | 21 | it 'initialize configuration' do 22 | expect(bitfinex.orderbook).not_to be_nil 23 | expect(bitfinex.instance_variable_get(:@market)).to eq(bitfinex_config['market']) 24 | end 25 | 26 | it '#build_order with positive amount' do 27 | price, _count, amount = data_create 28 | order = bitfinex.build_order(data_create) 29 | 30 | expect(order.price).to eq(price) 31 | expect(order.amount).to eq(amount) 32 | expect(order.side).to eq(:buy) 33 | expect(order.market).to eq(bitfinex_config['market']) 34 | end 35 | 36 | it '#build_order with negative amount' do 37 | price, _count, amount = data_create_sell 38 | order = bitfinex.build_order(data_create_sell) 39 | 40 | expect(order.price).to eq(price) 41 | expect(order.amount).to eq(-amount) 42 | expect(order.side).to eq(:sell) 43 | expect(order.market).to eq(bitfinex_config['market']) 44 | end 45 | 46 | it '#process_data creates order' do 47 | expect(bitfinex.orderbook).to receive(:update) 48 | bitfinex.process_data(data_create) 49 | end 50 | 51 | it '#process_data deletes order' do 52 | expect(bitfinex.orderbook).to receive(:delete) 53 | bitfinex.process_data(data_delete) 54 | end 55 | end 56 | 57 | context 'websocket messages processing' do 58 | let(:snapshot_message) do 59 | [69586, 60 | [ 61 | [22814737094, 141, 3.5459735], 62 | [22814761433, 141, 1678.72933611], 63 | [22814767295, 141, 2.28948142], 64 | [22814776345, 141, 572.89016357], 65 | [22814807549, 141, 6.848292], 66 | [22814800273, 141.01, -0.56493047], 67 | [22814805880, 141.01, -14], 68 | [22814813532, 141.01, -12.3841162], 69 | [22814813983, 141.01, -1.56], 70 | [22814799021, 141.1322692908, -2.32966444] 71 | ] 72 | ] 73 | end 74 | 75 | let(:single_order) { [69586, [22814737094, 141, 2.45]] } 76 | 77 | it 'processes single order in message' do 78 | # expect(bitfinex).to receive(:process_channel_message) 79 | expect(bitfinex).to receive(:process_data) 80 | 81 | bitfinex.process_message(single_order) 82 | end 83 | 84 | it 'processes list of orders' do 85 | n = snapshot_message[1].length 86 | expect(bitfinex).to receive(:process_data).exactly(n).times 87 | 88 | bitfinex.process_message(snapshot_message) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/arke/exchange/rubykube_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Exchange::Rubykube do 2 | include_context 'mocked rubykube' 3 | 4 | before(:all) { Arke::Log.define } 5 | 6 | let(:strategy_config) { {} } 7 | let(:exchange_config) { 8 | { 9 | 'driver' => 'rubykube', 10 | 'host' => 'http://www.devkube.com', 11 | 'key' => @authorized_api_key, 12 | 'secret' => SecureRandom.hex 13 | } 14 | } 15 | let(:strategy) { Arke::Strategy::Copy.new(strategy_config) } 16 | let(:order) { Arke::Order.new('ethusd', 1, 1, :buy) } 17 | let(:rubykube) { Arke::Exchange::Rubykube.new(exchange_config) } 18 | 19 | context 'rubykube#create_order' do 20 | it 'sets proper url when create order' do 21 | response = rubykube.create_order(order) 22 | 23 | expect(response.env.url.to_s).to include('peatio/market/orders') 24 | end 25 | 26 | it 'sets proper header when create order' do 27 | response = rubykube.create_order(order) 28 | 29 | expect(response.env.request_headers.keys).to include('X-Auth-Apikey', 'X-Auth-Nonce', 'X-Auth-Signature', 'Content-Type') 30 | expect(response.env.request_headers).to include('X-Auth-Apikey' => @authorized_api_key) 31 | end 32 | 33 | it 'gets 403 on request with wrong api key' do 34 | rubykube.instance_variable_set(:@api_key, SecureRandom.hex) 35 | 36 | expect { rubykube.create_order(order) }.to output(/Code: 403/).to_stderr_from_any_process 37 | end 38 | 39 | it 'updates open_orders after create' do 40 | rubykube.create_order(order) 41 | 42 | expect(rubykube.open_orders.contains?(order.side, order.price)).to eq(true) 43 | end 44 | end 45 | 46 | # context 'rubykube#stop_order' do 47 | # it 'sets proper url when stop order' do 48 | # response = rubykube.stop_order(order) 49 | 50 | # expect(response.env.url.to_s).to match(/peatio\/market\/orders\/\d+\/cancel/) 51 | # end 52 | 53 | # it 'sets proper header when stop order' do 54 | # response = rubykube.stop_order(order) 55 | 56 | # expect(response.env.request_headers.keys).to include('X-Auth-Apikey', 'X-Auth-Nonce', 'X-Auth-Signature', 'Content-Type') 57 | # expect(response.env.request_headers).to include('X-Auth-Apikey' => @authorized_api_key) 58 | # end 59 | # end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/arke/open_orders_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::OpenOrders do 2 | let(:market) { 'ethusd' } 3 | let(:open_orders) { Arke::OpenOrders.new(market) } 4 | let(:delete_order) { { id: 1, order: Arke::Order.new(market, 100, 30, :buy) } } 5 | let(:skip_order) { { id: 2, order: Arke::Order.new(market, 200, 20, :sell) } } 6 | let(:create_order) { { id: 3, order: Arke::Order.new(market, 250, 20, :sell) } } 7 | let(:update_order) { { id: 4, order: Arke::Order.new(market, 500, 10, :buy) } } 8 | let(:update_order_ob) { { id: 5, order: Arke::Order.new(market, 500, 5, :buy) } } 9 | 10 | let(:orderbook) do 11 | orderbook = Arke::Orderbook.new(market) 12 | 13 | orderbook.update(skip_order[:order]) 14 | orderbook.update(create_order[:order]) 15 | orderbook.update(update_order_ob[:order]) 16 | 17 | orderbook 18 | end 19 | 20 | it '#contains?' do 21 | order = skip_order[:order] 22 | 23 | open_orders.add_order(order, skip_order[:id]) 24 | 25 | expect(open_orders.contains?(order.side, order.price)).to eq(true) 26 | expect(open_orders.contains?(order.side, order.price + 100)).to eq(false) 27 | end 28 | 29 | it '#remove_order' do 30 | order = skip_order[:order] 31 | open_orders.add_order(order, skip_order[:id]) 32 | expect(open_orders.contains?(order.side, order.price)).to eq(true) 33 | open_orders.remove_order(skip_order[:id]) 34 | expect(open_orders.contains?(order.side, order.price)).to eq(false) 35 | end 36 | 37 | it '#price_amount' do 38 | order = skip_order[:order] 39 | open_orders.add_order(order, 33) 40 | open_orders.add_order(order, 34) 41 | 42 | expect(open_orders.price_amount(order.side, order.price)).to eq(2 * order.amount) 43 | end 44 | 45 | context 'open_orders#get_diff' do 46 | it 'return correct diff' do 47 | open_orders.add_order(delete_order[:order], delete_order[:id]) 48 | open_orders.add_order(update_order[:order], update_order[:id]) 49 | open_orders.add_order(skip_order[:order], skip_order[:id]) 50 | 51 | diff = open_orders.get_diff(orderbook) 52 | 53 | expect(diff[:create][create_order[:order].side].length).to eq(1) 54 | expect(diff[:create][create_order[:order].side][0].price).to eq(create_order[:order].price) 55 | expect(diff[:create][create_order[:order].side][0].amount).to eq(create_order[:order].amount) 56 | 57 | expect(diff[:update][update_order[:order].side].length).to eq(1) 58 | expect(diff[:update][update_order[:order].side][0].price).to eq(update_order[:order].price) 59 | expect(diff[:update][update_order[:order].side][0].amount) 60 | .to eq(update_order_ob[:order].amount - update_order[:order].amount) 61 | 62 | expect(diff[:delete][delete_order[:order].side].length).to eq(1) 63 | expect(diff[:delete][delete_order[:order].side][0]).to eq(delete_order[:id]) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/arke/orderbook_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Orderbook do 2 | let(:market) { 'ethusd' } 3 | let(:orderbook) { Arke::Orderbook.new(market) } 4 | 5 | it 'creates orderbook' do 6 | orderbook = Arke::Orderbook.new(market) 7 | 8 | expect(orderbook.book).to include({ sell: ::RBTree.new }) 9 | expect(orderbook.book).to include({ index: ::RBTree.new }) 10 | end 11 | 12 | context 'orderbook#add' do 13 | let(:order_buy) { Arke::Order.new(market, 1, 1, :buy) } 14 | let(:order_sell) { Arke::Order.new(market, 1, 1, :sell) } 15 | let(:order_sell2) { Arke::Order.new(market, 1, 1, :sell) } 16 | 17 | it 'adds buy order to orderbook' do 18 | orderbook.update(order_buy) 19 | 20 | expect(orderbook.book[:buy]).not_to be_nil 21 | expect(orderbook.book[:buy][order_buy.price]).not_to be_nil 22 | end 23 | 24 | it 'adds sell order to orderbook' do 25 | orderbook.update(order_sell) 26 | 27 | expect(orderbook.book[:sell]).not_to be_nil 28 | expect(orderbook.book[:sell][order_sell.price]).not_to be_nil 29 | end 30 | 31 | it 'updates order with the same price' do 32 | orderbook.update(order_sell) 33 | orderbook.update(order_sell2) 34 | 35 | expect(orderbook.book[:sell][order_sell.price]).to eq(order_sell2.amount) 36 | end 37 | end 38 | 39 | context 'orderbook#contains?' do 40 | let(:order0) { Arke::Order.new(market, 5, 1, :buy) } 41 | let(:order1) { Arke::Order.new(market, 8, 1, :buy) } 42 | 43 | it 'returns true if order is in orderbook' do 44 | orderbook.update(order0) 45 | orderbook.update(order1) 46 | 47 | expect(orderbook.contains?(order0)).to equal(true) 48 | expect(orderbook.contains?(order1)).to equal(true) 49 | end 50 | 51 | it 'returns false if order is not in orderbook' do 52 | expect(orderbook.contains?(order0)).to equal(false) 53 | end 54 | end 55 | 56 | context 'orderbook#get' do 57 | let(:order_sell_0) { Arke::Order.new('ethusd', 5, 1, :sell) } 58 | let(:order_sell_1) { Arke::Order.new('ethusd', 8, 1, :sell) } 59 | let(:order_sell_cheap) { Arke::Order.new('ethusd', 2, 1, :sell) } 60 | let(:order_buy_0) { Arke::Order.new('ethusd', 5, 1, :buy) } 61 | let(:order_buy_1) { Arke::Order.new('ethusd', 8, 1, :buy) } 62 | let(:order_buy_expensive) { Arke::Order.new('ethusd', 9, 1, :buy) } 63 | 64 | it 'gets order with the lowest price for sell side' do 65 | orderbook.update(order_sell_0) 66 | orderbook.update(order_sell_1) 67 | orderbook.update(order_sell_cheap) 68 | 69 | expect(orderbook.get(:sell)[0]).to equal(order_sell_cheap.price) 70 | end 71 | 72 | it 'gets order with the highest price for buy side' do 73 | orderbook.update(order_buy_0) 74 | orderbook.update(order_buy_1) 75 | orderbook.update(order_buy_expensive) 76 | 77 | expect(orderbook.get(:buy)[0]).to equal(order_buy_expensive.price) 78 | end 79 | end 80 | 81 | context 'orderbook#remove' do 82 | let(:order_buy) { Arke::Order.new(market, 1, 1, :buy) } 83 | 84 | it 'removes correct order from orderbook' do 85 | orderbook.update(order_buy) 86 | orderbook.update(Arke::Order.new(market, order_buy.price, 1, :buy)) 87 | orderbook.update(Arke::Order.new(market, 11, 1, :sell)) 88 | 89 | orderbook.delete(order_buy) 90 | 91 | expect(orderbook.contains?(order_buy)).to eq(false) 92 | expect(orderbook.book[:buy][order_buy.price]).to be_nil 93 | expect(orderbook.book[:sell]).not_to be_nil 94 | end 95 | 96 | it 'does nothing if non existing id' do 97 | orderbook.update(order_buy) 98 | 99 | orderbook.delete(Arke::Order.new(market, 10, 1, :buy)) 100 | 101 | expect(orderbook.book[:buy]).not_to be_nil 102 | expect(orderbook.contains?(order_buy)).to eq(true) 103 | end 104 | end 105 | 106 | context 'orderbook#merge' do 107 | let(:ob1) { Arke::Orderbook.new(market) } 108 | let(:ob2) { Arke::Orderbook.new(market) } 109 | let(:ob3) { Arke::Orderbook.new(market) } 110 | 111 | it 'merges two orderbooks into one' do 112 | ob1.update(Arke::Order.new(market, 10, 10, :sell)) 113 | ob1.update(Arke::Order.new(market, 20, 15, :sell)) 114 | ob1.update(Arke::Order.new(market, 25, 5, :sell)) 115 | 116 | ob2.update(Arke::Order.new(market, 10, 30, :sell)) 117 | ob2.update(Arke::Order.new(market, 20, 20, :sell)) 118 | ob2.update(Arke::Order.new(market, 10, 10, :buy)) 119 | 120 | ob3.update(Arke::Order.new(market, 10, 40, :sell)) 121 | ob3.update(Arke::Order.new(market, 20, 35, :sell)) 122 | ob3.update(Arke::Order.new(market, 25, 5, :sell)) 123 | ob3.update(Arke::Order.new(market, 10, 10, :buy)) 124 | 125 | ob1.merge!(ob2) 126 | 127 | expect(ob1.book[:index]).to eq(ob3.book[:index]) 128 | expect(ob1.book[:sell]).to eq(ob3.book[:sell]) 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/arke/reactor_spec.rb: -------------------------------------------------------------------------------- 1 | describe Arke::Reactor do 2 | let(:config) { YAML.load_file('spec/support/fixtures/test_config.yaml') } 3 | let(:reactor) { Arke::Reactor.new(config) } 4 | 5 | it 'inits configuration' do 6 | expect(reactor.instance_variable_get(:@strategy)).to be_instance_of(Arke::Strategy.strategy_class(config['type'])) 7 | end 8 | 9 | it '#build_dax' do 10 | reactor.build_dax(config) 11 | dax = reactor.instance_variable_get(:@dax) 12 | 13 | # expect(dax).to include(:target) 14 | config['sources'].each do |source| 15 | expect(dax).to include(source['driver'].to_sym) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | 17 | $LOAD_PATH << File.expand_path('../lib', __dir__) 18 | $LOAD_PATH << File.expand_path('../lib/arke', __dir__) 19 | 20 | require 'simplecov' 21 | require 'simplecov-rcov' 22 | 23 | SimpleCov.start do 24 | add_filter '/spec/' 25 | track_files 'lib/**/*.rb' 26 | end 27 | 28 | SimpleCov.formatters = [ 29 | SimpleCov::Formatter::HTMLFormatter, 30 | SimpleCov::Formatter::RcovFormatter 31 | ] 32 | 33 | require 'arke' 34 | require 'arke/command' 35 | 36 | require 'pry' 37 | 38 | Dir['./spec/support/**/*.rb'].each { |f| require f } 39 | 40 | RSpec.configure do |config| 41 | # rspec-expectations config goes here. You can use an alternate 42 | # assertion/expectation library such as wrong or the stdlib/minitest 43 | # assertions if you prefer. 44 | config.expect_with :rspec do |expectations| 45 | # This option will default to `true` in RSpec 4. It makes the `description` 46 | # and `failure_message` of custom matchers include text for helper methods 47 | # defined using `chain`, e.g.: 48 | # be_bigger_than(2).and_smaller_than(4).description 49 | # # => "be bigger than 2 and smaller than 4" 50 | # ...rather than: 51 | # # => "be bigger than 2" 52 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 53 | end 54 | 55 | # rspec-mocks config goes here. You can use an alternate test double 56 | # library (such as bogus or mocha) by changing the `mock_with` option here. 57 | config.mock_with :rspec do |mocks| 58 | # Prevents you from mocking or stubbing a method that does not exist on 59 | # a real object. This is generally recommended, and will default to 60 | # `true` in RSpec 4. 61 | mocks.verify_partial_doubles = true 62 | end 63 | 64 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 65 | # have no way to turn it off -- the option exists only for backwards 66 | # compatibility in RSpec 3). It causes shared context metadata to be 67 | # inherited by the metadata hash of host groups and examples, rather than 68 | # triggering implicit auto-inclusion in groups with matching metadata. 69 | config.shared_context_metadata_behavior = :apply_to_host_groups 70 | 71 | # The settings below are suggested to provide a good initial experience 72 | # with RSpec, but feel free to customize to your heart's content. 73 | =begin 74 | # This allows you to limit a spec run to individual examples or groups 75 | # you care about by tagging them with `:focus` metadata. When nothing 76 | # is tagged with `:focus`, all examples get run. RSpec also provides 77 | # aliases for `it`, `describe`, and `context` that include `:focus` 78 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 79 | config.filter_run_when_matching :focus 80 | 81 | # Allows RSpec to persist some state between runs in order to support 82 | # the `--only-failures` and `--next-failure` CLI options. We recommend 83 | # you configure your source control system to ignore this file. 84 | config.example_status_persistence_file_path = "spec/examples.txt" 85 | 86 | # Limits the available syntax to the non-monkey patched syntax that is 87 | # recommended. For more details, see: 88 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 89 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 90 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 91 | config.disable_monkey_patching! 92 | 93 | # This setting enables warnings. It's recommended, but in some cases may 94 | # be too noisy due to issues in dependencies. 95 | config.warnings = true 96 | 97 | # Many RSpec users commonly either run the entire suite or an individual 98 | # file, and it's useful to allow more verbose output when running an 99 | # individual spec file. 100 | if config.files_to_run.one? 101 | # Use the documentation formatter for detailed output, 102 | # unless a formatter has already been configured 103 | # (e.g. via a command-line flag). 104 | config.default_formatter = "doc" 105 | end 106 | 107 | # Print the 10 slowest examples and example groups at the 108 | # end of the spec run, to help surface which specs are running 109 | # particularly slow. 110 | config.profile_examples = 10 111 | 112 | # Run specs in random order to surface order dependencies. If you find an 113 | # order dependency and want to debug it, you can fix the order by providing 114 | # the seed, which is printed after each run. 115 | # --seed 1234 116 | config.order = :random 117 | 118 | # Seed global randomization in this process using the `--seed` CLI option. 119 | # Setting this allows you to use `--seed` to deterministically reproduce 120 | # test failures related to randomization by passing the same `--seed` value 121 | # as the one that triggered the failure. 122 | Kernel.srand config.seed 123 | =end 124 | end 125 | -------------------------------------------------------------------------------- /spec/support/fixtures/bitfinex.yaml: -------------------------------------------------------------------------------- 1 | [145872,[[22847510010,138.78,12], 2 | [22847510020,138.76,1.79992078], 3 | [22847508100,138.75,1.17861594], 4 | [22847509249,138.74,0.54925524], 5 | [22847471837,138.7,81.62644284], 6 | [22847475891,138.7,5], 7 | [22847509764,138.7,15], 8 | [22847507813,138.66,7.68916607], 9 | [22847508747,138.66,7.67226826], 10 | [22847500419,138.65,8.20838373], 11 | [22847508473,138.63,1.07965077], 12 | [22847393568,138.624224365,3], 13 | [22847475465,138.62,1.8007635], 14 | [22847031050,138.6,14.408], 15 | [22847501362,138.6,10.49126974], 16 | [22847500105,138.56,21], 17 | [22847414524,138.55,5], 18 | [22847509300,138.55,2.15953141], 19 | [22847509771,138.54,30.0572268], 20 | [22847509114,138.53,29.46743866], 21 | [22847499956,138.4812288591,2.86875448], 22 | [22847501443,138.48,8.7379912], 23 | [22847499769,138.45357493,4.92125984], 24 | [22847228264,138.45,2.63979889], 25 | [22847499968,138.44,7.20980533], 26 | [22847508592,138.86,-3], 27 | [22847505815,138.87,-20], 28 | [22847493004,138.9,-5.62687396], 29 | [22847503766,138.9,-8.35218801], 30 | [22847393569,138.90864009,-3], 31 | [22847428379,138.931393348,-11.78032764], 32 | [22847505992,138.93655665,-12.42884094], 33 | [22847255074,138.9454413876,-5.62737242], 34 | [22847504057,138.95,-7.53645122], 35 | [22847508909,138.96,-10.96620486], 36 | [22847255093,138.965523235,-5.62600117], 37 | [22847508751,138.97,-12.5239233], 38 | [22847428377,138.9719678178,-12.54650362], 39 | [22847133537,138.97245525,-5.62625951], 40 | [22847503847,138.99,-8.60683195], 41 | [22846742832,139,-14.407], 42 | [22847428368,139.05,-10.91316879], 43 | [22847504582,139.06,-8.67505323], 44 | [22846867806,139.07,-2.8], 45 | [22847506818,139.07,-106.92938308], 46 | [22847503504,139.079289525,-363.42104327], 47 | [22847503897,139.1,-27], 48 | [22847509611,139.12,-7.88014469], 49 | [22847416593,139.13,-2.59756031], 50 | [22847509867,139.13,-22.5]]] -------------------------------------------------------------------------------- /spec/support/fixtures/test_config.yaml: -------------------------------------------------------------------------------- 1 | type: copy 2 | target: 3 | driver: bitfaker 4 | market: 'ETHUSD' 5 | host: "http://www.devkube.com" 6 | key: "" 7 | secret: "" 8 | rate_limit: 0.2 9 | sources: 10 | - driver: bitfaker 11 | market: 'ETHUSD' 12 | host: "api.bitfinex.com" 13 | key: "" 14 | secret: "" 15 | rate_limit: 0.2 16 | -------------------------------------------------------------------------------- /spec/support/mocked_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webmock/rspec' 4 | 5 | shared_context 'mocked rubykube' do 6 | before(:each) do 7 | # TODO: find better way to store it (let is not accassible inside before) 8 | @authorized_api_key = '3107c98eb442e4135541d434410aaaa6' 9 | authorized_header = { 'X-Auth-Apikey'=> @authorized_api_key } 10 | 11 | # non-authorized requests 12 | 13 | stub_request(:post, /peatio\/market\/orders/). 14 | to_return(status: 403, body: '', headers: {}) 15 | 16 | # authorized requests 17 | 18 | stub_request(:get, /peatio\/public\/timestamp/). 19 | with(headers: authorized_header). 20 | to_return(status: 200, body: '', headers: {}) 21 | 22 | stub_request(:post, /peatio\/market\/orders/). 23 | with(headers: authorized_header). 24 | to_return(status: 201, body: { id: Random.rand(1...1000) }.to_json, headers: {}) 25 | 26 | stub_request(:post, /peatio\/market\/orders\/\d+\/cancel/). 27 | with(headers: authorized_header). 28 | to_return(status: 201, body: '', headers: {}) 29 | end 30 | end 31 | 32 | shared_context 'mocked binance' do 33 | before(:each) do 34 | @authorized_api_key = 'Uwg8wqlxueiLCsbTXjlogviL8hdd60' 35 | authorized_headers = { 'X-MBX-APIKEY' => @authorized_api_key, 'Content-Type' => 'application/x-www-form-urlencoded' } 36 | 37 | stub_request(:post, 'https://api.binance.com/api/v3/order'). 38 | to_return(status: 401, body: 'Unauthorized', headers: {}) 39 | 40 | stub_request(:get, "https://www.binance.com/api/v1/depth?symbol=ETHUSDT&limit=1000"). 41 | to_return( 42 | status: 200, 43 | body: { 44 | "lastUpdateId"=>320927259, 45 | "bids"=>[["135.87000000", "36.43875000", []], ["135.85000000", "0.57176000", []], ["135.84000000", "6.62227000", []]], 46 | "asks"=>[["135.91000000", "0.00070000", []], ["135.93000000", "8.00000000", []], ["135.95000000", "1.11699000", []]] 47 | }.to_json, 48 | headers: {} 49 | ) 50 | 51 | stub_request(:post, 'https://api.binance.com/api/v3/order'). 52 | with(headers: authorized_headers). 53 | to_return(status: 200, body: '', headers: {}) 54 | end 55 | end 56 | --------------------------------------------------------------------------------