Browse Source

v1

master
Rafael Polo 2 months ago
commit
3aaafbcfc5
10 changed files with 547 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +9
    -0
      README.md
  3. +116
    -0
      arbitro.cr
  4. +218
    -0
      broker.cr
  5. +20
    -0
      config.cr
  6. +156
    -0
      helper.cr
  7. +15
    -0
      order.cr
  8. BIN
      sample.png
  9. +6
    -0
      shard.lock
  10. +6
    -0
      shard.yml

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
lib/

+ 9
- 0
README.md View File

@ -0,0 +1,9 @@
## Arbitro
A ideia de arbitragem nos fascinou pelo tempo necessário para desenvolver um sistema que em seus tempos áureos chegava a faturar 6% entre a Kraken e a Braziliex.
---
#### Exemplo
![arbitro](sample.png)

+ 116
- 0
arbitro.cr View File

@ -0,0 +1,116 @@
require "./helper"
require "./broker"
Config.params[:cache] = false
Config.params[:eur_brl] = get_last_eur_brl
puts "EUR:BRL : #{Config.params[:eur_brl]}".colorize(:light_red)
puts line
def arbitrage_brokers(b1, b2, coins = [] of String)
result = [] of String | Colorize::Object(String)
t_s = 0.0
coins = (b1.coins & b2.coins) if coins.empty?
coins.each do |coin|
b1_book = b1.book(coin)
b2_book = b2.book(coin)
begin
price_buy = b1_book.sort_by{|o| o.value}.select{|o| o.type=="ask"}.first.value
price_sell = b2_book.sort_by{|o| o.value}.select{|o| o.type=="bid"}.last.value
rescue
# when any is empty
price_buy = 0
price_sell = 0
end
perc = percentage(price_buy, price_sell).round(3)
if perc >= 0
result << "| #{coin.to_s.upcase} | #{b1.broker_name} -> #{b2.broker_name} | #{price_buy.round(4)} : #{price_sell.round(4)} | #{perc}%".colorize(:magenta)
table = TerminalTable.new
table.headings = ["b1 buy < b2 sell", "total", "%", "profit"]
sum = 0
perc_t = 0
count = 0
# vendem em b1 por menos que compram em b2
b1_book.sort_by!{|o| o.value}.select{|o|o.type=="ask" && o.value <= price_sell}.each do |o|
perc = percentage(o.value, price_sell).to_f64 # taxas 0.16+0.16+0.5+0.5+send
if perc >= 0.5
count+=1
table << [o.value.round(4), "#{o.price.round(2)}", perc.round(2), "#{profit_perct(o.price, perc)}"]
sum += o.price
perc_t += perc
end
end
avg_perc = perc_t/count
profit = profit_perct(sum, avg_perc)
t_s += profit.to_f64
# options?
if count>0
# begin
# fees = get_json("https://braziliex.com", "/api/v1/public/currencies")[coin]
# result << "MinWithdrawal #{fees["MinWithdrawal"]}".colorize(:light_red)
# result << "txWithdrawalFee #{fees["txWithdrawalFee"]} = €#{to_eur(fees["txWithdrawalFee"].to_s.to_f64)}".colorize(:light_red)
# result << "minDeposit #{fees["minDeposit"]} = €#{to_eur(fees["txWithdrawalFee"].to_s.to_f64)}".colorize(:light_red)
# result << "minAmountTradeUSDT #{fees["minAmountTradeUSDT"]}".colorize(:light_red)
# end
result << table.render.colorize(:blue)
# past buys
trades = TerminalTable.new
trades.headings = ["b1 bought", "total", "%", "ago"]
t = b1.trades(coin).select{|o| o.type=="bought"}[0..5].each do |t|
trades << [t.value.round(4), "#{t.price.round(2)}", percentage(t.value, price_sell).to_f64, "#{ago(t.utc)}"]
end
result << trades.render.colorize(:yellow)
# next buys
table = TerminalTable.new
table.headings = ["b2 buy > b1 ask", "total", "%", "profit"]
# orders b2 buying > b1 selling
b2_book.sort_by!{|o| o.value}.select{|o|o.type=="bid" && o.value >= price_buy}.reverse[0..10].each do |o|
perc = percentage(price_buy, o.value).to_f64 # taxas 0.16+0.16+0.5+0.5+send
table << [o.value.round(4), "#{o.price.round(2)}", perc.round(2), "#{profit_perct(o.price, perc)}"]
end
result << table.render.colorize(:green)
# past sells
trades = TerminalTable.new
trades.headings = ["b2 sold", "total", "%", "ago"]
t = b2.trades(coin).select{|o| o.type=="sold"}[0..5].each do |t|
trades << [t.value.round(4), "#{t.price.round(2)}", percentage(price_buy, t.value).to_f64, "#{ago(t.utc)}"]
end
result << trades.render.colorize(:red)
result << "=> invest €#{sum.round(2)} to get +#{avg_perc.round(2)}% = #{profit}".colorize(:green)
result << line
end
if t_s > 0
result << "=> total €#{t_s.round(2)}".colorize(:yellow)
result << line
end
end
end
result.join("\n")
end
def do_it_all!
perms = [Broker::Kraken.new, Broker::Braziliex.new].permutations(2)
channel = Channel(String).new(perms.size)
perms.each do |duo|
spawn { channel.send arbitrage_brokers(duo[0], duo[1]) }
end
perms.size.times.each{ puts channel.receive unless channel.receive.empty? }
end
do_it_all!
# puts arbitrage_brokers(Broker::Braziliex.new, Broker::Kraken.new, ["dai"])
# test_colors
puts "\t done in #{elapsed_time}".colorize(:blue)
puts line

+ 218
- 0
broker.cr View File

@ -0,0 +1,218 @@
module Exchange
property coins = Array(String).new
abstract def book(coin)
abstract def trades(coin)
def broker_name
self.class.name.gsub("Broker::", "")
end
end
module Broker
class Kraken
include Exchange
@coins =
["eth","btc","xrp","ltc","usdt","bch","eur","rep","zec","ada","qtum","xtz","atom","gno","eos","mln","bat","waves","icx","link","sc","omg","paxg","nano","lsk","algo","trx","oxt","kava","comp","storj","knc","repv2","xmr","xlm","dash","dai","usdc", "xdg"]
COIN_CODE = {
"eth": "XETHZEUR",
"btc": "XXBTZEUR",
"xrp": "XXRPZEUR",
"ltc": "XLTCZEUR",
"usdt": "USDTEUR",
"bch": "BCHEUR",
"eur": "EUREUR",
"rep": "REPEUR",
"zec": "XZECZEUR",
"ada": "ADAEUR",
"qtum": "QTUMEUR",
"xtz": "XTZEUR",
"atom": "ATOMEUR",
"gno": "GNOEUR",
"eos": "EOSEUR",
"mln": "MLNEUR",
"bat": "BATEUR",
"waves": "WAVESEUR",
"icx": "ICXEUR",
"link": "LINKEUR",
"sc": "SCEUR",
"omg": "OMGEUR",
"paxg": "PAXGEUR",
"nano": "NANOEUR",
"lsk": "LSKEUR",
"algo": "ALGOEUR",
"trx": "TRXEUR",
"oxt": "OXTEUR",
"kava": "KAVAEUR",
"comp": "COMPEUR",
"storj": "STORJEUR",
"knc": "KNCEUR",
"repv2": "REPV2EUR",
"xmr": "XXMRZEUR",
"xdg": "XDGEUR",
"xlm": "XXLMZEUR",
"dash": "DASHEUR",
"dai": "DAIEUR",
"usdc": "USDCEUR"
}
def book(coin)
url_coin = coin=="btc" ? "xbt" : coin
book = get_json("https://api.kraken.com", "/0/public/Depth?pair=#{url_coin}eur")["result"][COIN_CODE[coin]]
orders = Array(Order).new
asks = book["asks"]
(0..asks.size-1).each do |i|
order = asks[i]
# type, value, quantity, time
orders << Order.new("ask", f(order[0]), (f(order[1]) * f(order[0])), order[2].to_s)
end
bids = book["bids"]
(0..bids.size-1).each do |i|
order = bids[i]
# type, value, quantity, time
orders << Order.new("bid", f(order[0]), (f(order[1]) * f(order[0])), order[2].to_s)
end
orders.sort_by!{|o| o.utc}.reverse
end
def trades(coin)
url_coin = coin=="btc" ? "xbt" : coin
trades_json = get_json("https://api.kraken.com", "/0/public/Trades?pair=#{url_coin}eur")["result"][COIN_CODE[coin]]
trades = Array(Order).new
(0..trades_json.size-1).each do |i|
trade = trades_json[i]
type = trade[3].to_s == "s" ? "sold" : "bought"
trades << Order.new(type, f(trade[0]), (f(trade[1]) * f(trade[0])), trade[2].to_s)
end
trades.sort_by{|o| o.utc}.reverse
end
end
class NovaDAX
include Exchange
@coins = ["btc", "dgb", "dcr", "eth", "bsv", "xmr", "bch", "xlm", "xrp", "waves", "etc", "trx", "btt", "eos", "ada", "bnb", "link", "dash", "ltc", "xtz", "iota", "omg", "doge", "nuls", "brz"]
def book(coin)
book = get_json("https://api.novadax.com", "/v1/market/depth?symbol=#{coin.upcase}_BRL&limit=20")["data"]
orders = Array(Order).new
asks = book["asks"]
(0..asks.size-1).each do |i|
order = asks[i]
# type, value, quantity, time
orders << Order.new("ask", to_eur(f(order[0])), to_eur(f(order[1]) * f(order[0])), Time.utc.to_unix_ms.to_s)
end
bids = book["bids"]
(0..bids.size-1).each do |i|
order = bids[i]
# type, value, quantity, time
orders << Order.new("bid", to_eur(f(order[0])), to_eur(f(order[1]) * f(order[0])), Time.utc.to_unix_ms.to_s)
end
orders.sort_by!{|o| o.utc}.reverse
end
def trades(coin)
trades_json = get_json("https://api.novadax.com", "/v1/market/trades?symbol=#{coin.upcase}_BRL&limit=20")["data"]
trades = Array(Order).new
(0..trades_json.size-1).each do |i|
trade = trades_json[i]
type = trade["side"].to_s == "SELL" ? "sold" : "bought"
trades << Order.new(type, to_eur(f(trade["price"])), to_eur(f(trade["amount"]) * f(trade["price"])), trade["timestamp"].to_s)
end
trades.sort_by!{|t| t.utc}.reverse
end
end
class Braziliex
include Exchange
@coins =
["brl","btc","bch","ltc","eth","dash","zec","dcr","xrp","usdt","tusd","zrx","dai","paxg","sngls","brzx","gmr","omg","gnt","epc","abc","mxt","smart","nbr","bsv","crw","lcc","onix","etc","iop","btg","bnb","eos","prsp","cfty","trx"]
def fees
result = Hash(String, Float64).new
fees_json = get_json("https://braziliex.com", "/api/v1/public/currencies")
coins.each do |c|
result[c] = to_eur fees_json[c]["txWithdrawalFee"].to_s.to_f64
end
result
end
def book(coin)
book = get_json("https://braziliex.com", "/api/v1/public/orderbook/#{coin}_brl")
orders = Array(Order).new
asks = book["asks"]
(0..asks.size-1).each do |i|
order = asks[i]
# type, value, quantity, time
eur_price = to_eur(f(order["price"]))
orders << Order.new("ask", eur_price, f(order["amount"]) * eur_price, Time.utc.to_unix_ms.to_s)
end
bids = book["bids"]
(0..bids.size-1).each do |i|
order = bids[i]
# type, value, quantity, time
eur_price = to_eur(f(order["price"]))
orders << Order.new("bid", eur_price, f(order["amount"]) * eur_price, Time.utc.to_unix_ms.to_s)
end
orders.sort_by!{|o| o.utc}.reverse
end
def trades(coin)
trades_json = get_json("https://braziliex.com", "/api/v1/public/tradehistory/#{coin}_brl")
trades = Array(Order).new
(0..trades_json.size-1).each do |i|
trade = trades_json[i]
type = trade["type"].to_s == "sell" ? "sold" : "bought"
trades << Order.new(type, to_eur(f(trade["price"])), to_eur(f(trade["amount"]) * f(trade["price"])), trade["timestamp"].to_s)
end
trades.sort_by{|t| t.utc}.reverse
end
end
class MercadoBitcoin
include Exchange
@coins = ["btc", "ltc", "usdc", "xrp", "bch", "eth"]
def book(coin)
book = get_json("https://www.mercadobitcoin.net", "/api/#{coin}/orderbook/")
orders = Array(Order).new
asks = book["asks"]
(0..asks.size-1).each do |i|
order = asks[i]
# type, value, quantity, time
orders << Order.new("ask", to_eur(f(order[0])), to_eur(f(order[1]) * f(order[0])), Time.utc.to_unix_ms.to_s)
end
bids = book["bids"]
(0..bids.size-1).each do |i|
order = bids[i]
# type, value, quantity, time
orders << Order.new("bid", to_eur(f(order[0])), to_eur(f(order[1]) * f(order[0])), Time.utc.to_unix_ms.to_s)
end
orders.sort_by!{|o| o.utc}.reverse
end
def trades(coin)
trades_json = get_json("https://www.mercadobitcoin.net", "/api/#{coin}/trades/")
trades = Array(Order).new
(0..trades_json.size-1).each do |i|
trade = trades_json[i]
type = trade["type"].to_s == "sell" ? "sold" : "bought"
trades << Order.new(type, to_eur(f(trade["price"])), to_eur(f(trade["amount"]) * f(trade["price"])), trade["date"].to_s)
end
trades.sort_by{|t| t.utc}.reverse
end
end
end

+ 20
- 0
config.cr View File

@ -0,0 +1,20 @@
class Config
property params = {} of Symbol => (String | Int32 | Float64 | Bool)
def initialize
@params[:log] = false
@params[:simulate] = false
@params[:retry] = false
@params[:start_time] = now
end
def self.params
self.instance.params
end
# singleton class
def self.instance
@@instance ||= new
end
end

+ 156
- 0
helper.cr View File

@ -0,0 +1,156 @@
require "json"
require "http/client"
require "openssl/hmac"
require "terminal_table"
require "digest/md5"
require "colorize"
require "./order"
require "./config"
module Helper
def f(el)
el.to_s.to_f64
end
def z(time)
return time < 10 ? "0#{time}" : time
end
def ago(utc)
date = Time.utc unless utc
unix = utc.to_s.scan(/\d{10}/)
if !unix.empty? # unix time!
date = Time.unix(unix.[0][0].to_i64)
else
date = Time.parse(utc, "%Y-%m-%dT%H:%M:%S.%6N", Time::Location::UTC)
end
span = (Time.utc - date)
days = span.days > 0 ? "#{span.days}d" : ""
"#{days}#{z span.hours}h#{z span.minutes}m#{z span.seconds}s#{span.milliseconds}"
end
puts line
puts "\t Arbitro".colorize(:blue)
puts line
def line
"=================================".colorize(:blue)
end
def log(str)
pp str if Config.params[:log]
end
# finish! if interrupted
# [Signal::QUIT, Signal::ABRT, Signal::INT, Signal::KILL, Signal::TERM].each do |s|
# s.trap do
# puts "=> Interrupted!".colorize(:red)
# finish!
# end
# end
def get_json(url, path)
# puts "http://51.15.63.128:2020/?url=#{url}#{path}"
log "=> #{url}#{path}".colorize(:yellow)
md5 = Digest::MD5.hexdigest("#{url}#{path}")
cache = "cache/#{md5}.json"
json = ""
begin
if Config.params[:cache] && File.exists?(cache)
json = File.read(cache)
log md5
else
json = HTTP::Client.get("#{url}#{path}").body
File.write(cache, json)
end
rescue e
puts "error: #{e.message}".colorize(:red)
end
JSON.parse(json)
end
def get_last_eur_brl
get_json("https://economia.awesomeapi.com.br", "/eur")[0]["ask"].to_s.to_f64
# pctChange
end
def to_eur(f64)
f64 / f(Config.params[:eur_brl])
end
def test_colors
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :light_gray, :dark_gray, :light_red, :light_green, :light_yellow, :light_blue, :light_magenta, :light_cyan, :white]
colors.each do |c1|
puts "color #{c1}".colorize(c1)
colors.each do |c2|
puts "fore #{c1} | back #{c2}".colorize.fore(c1).back(c2)
end
end
end
def profit_perct(x, por)
(x.to_f64 * por.to_f64/100).round(2).to_s
end
def percentage(x, y)
x, y = x.to_f64, y.to_f64
pct = x < y ? ((y - x) / x) * 100.0 : - ((x - y) / y) * 100.0
pct = 0.0 if pct.infinite? || pct.nan?
pct.round(4)
end
def as_time(time, tmz=false)
parsed_time = Time.parse(time.to_s, "%Ft%T.%L", Time::Location::UTC)
tmz ? parsed_time + 2.hours : parsed_time # TMZ = 2.hours # athens
end
def elapsed_time
as_time_ago(Time.utc - as_time(Config.params[:start_time]))
end
def as_time_ago(span)
h = span.hours > 0 ? "#{span.hours}h" : ""
m = span.minutes > 0 ? "#{span.minutes}m" : ""
"#{h}#{m}#{span.seconds}s#{span.milliseconds}"
end
def as_dolar(value)
"$#{value.round(2)}"
end
# def finish!
# elapsed = Time.utc - as_time(Config.params[:start_time])
# line
# puts "=> #{Broker.requests_count} requests in #{as_time_ago(elapsed)}".colorize(:yellow)
# line
# exit
# end
def color(float : Float64)
float > 0 ? float.to_s.colorize(:green) : float.to_s.colorize(:red)
end
def now
Time.utc.to_s("%Ft%T.%L")
end
def save_json(json, file)
File.open("log/#{file.to_s}.json", "w") do |f|
f.write("#{json.to_pretty_json}\n".to_slice)
end
end
end
include Helper
class Logger
def self.log(msg, file=:renda, error=false)
File.open("log/#{file.to_s}.log", "a+") do |f|
stamped = "#{now} #{msg}"
puts stamped.colorize(error ? :red : :yellow) if Config.params[:log] || error
f.write("#{stamped}\n".to_slice)
end
end
end

+ 15
- 0
order.cr View File

@ -0,0 +1,15 @@
class Order
getter type, value, price, utc
def initialize(@type : String, @value : Float64, @price : Float64, @utc : String)
end
def to_s
{
type: @type,
value: @value,
price: "#{@price.round(2)}",
ago: ago(utc)
}
end
end

BIN
sample.png View File

Before After
Width: 368  |  Height: 766  |  Size: 48 KiB

+ 6
- 0
shard.lock View File

@ -0,0 +1,6 @@
version: 2.0
shards:
terminal_table:
git: https://github.com/benoist/terminal_table.cr.git
version: 0.1.0+git.commit.a14e244219a6a7b804b3491bafcff73f4b74855e

+ 6
- 0
shard.yml View File

@ -0,0 +1,6 @@
name: shards
version: 0.1.0
dependencies:
terminal_table:
github: benoist/terminal_table.cr

Loading…
Cancel
Save