Last active
April 16, 2024 07:08
-
-
Save codedot/e2e707ac1ad95f5ac2cd to your computer and use it in GitHub Desktop.
Automated multi-currency market maker for Ripple
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
var ripple = require("ripple-lib"); | |
var assert = require("assert"); | |
var fs = require("fs"); | |
var util = require("util"); | |
var options = { | |
max_fee: 12000, | |
fee_cushion: 1.2, | |
servers: [ | |
'wss://s-east.ripple.com:443', | |
'wss://s-west.ripple.com:443' | |
], | |
trusted: false | |
}; | |
var remote = new ripple.Remote(options); | |
var maxfee = options.max_fee / 1e6; | |
var env = process.env; | |
var id = env.RIPPLE_ID; | |
var timeout = 3e5; | |
var filename = "state.json"; | |
var minstake, maxstake, stake; | |
var state, saldo, queue, count, ledger, pairs, npairs, bug; | |
function debug() | |
{ | |
var options = { | |
colors: true, | |
depth: 6 | |
}; | |
var str = util.inspect(arguments, options); | |
util.error(str); | |
} | |
function load() | |
{ | |
try { | |
var state = fs.readFileSync(filename); | |
state = JSON.parse(state); | |
assert(state instanceof Array); | |
assert(state.length); | |
return state; | |
} catch (error) { | |
console.log("History unavailable"); | |
return []; | |
} | |
} | |
function save() | |
{ | |
state = JSON.stringify(state); | |
fs.writeFileSync(filename, state); | |
} | |
function now() | |
{ | |
var date = new Date(); | |
return date.getTime(); | |
} | |
function start() | |
{ | |
saldo = {}; | |
queue = []; | |
count = 0; | |
remote.request_ledger_closed(getsaldo); | |
} | |
function getsaldo(error, response) | |
{ | |
debug(error, response); | |
if (error) { | |
console.log("Failed to get ledger"); | |
return start(); | |
} | |
ledger = response.ledger_index; | |
console.log("Ledger", ledger); | |
remote.request_account_balance(id, ledger, setxrp); | |
} | |
function setxrp(error, response) | |
{ | |
var balance; | |
debug(error, response); | |
if (error) { | |
console.log("Failed to get balance"); | |
return start(); | |
} | |
balance = response.to_number(); | |
saldo["XRP"] = balance / 1e6; | |
remote.request_account_lines(id, ledger, setlines); | |
} | |
function ispair(src, dst) | |
{ | |
if (src.unit == dst.unit) | |
return false; | |
if ("XRP" == src.currency) | |
return true; | |
if ("XRP" == dst.currency) | |
return true; | |
if (src.currency == dst.currency) | |
return true; | |
if (src.issuer == dst.issuer) | |
return true; | |
return false; | |
} | |
function getpairs() | |
{ | |
var list = []; | |
var base, unit; | |
for (base in saldo) { | |
var src = {}; | |
var counter; | |
unit = base.split(":"); | |
src.currency = unit.shift(); | |
src.issuer = unit.shift(); | |
src.unit = base; | |
for (counter in saldo) { | |
var dst = {}; | |
var pair = base.concat(">", counter); | |
unit = counter.split(":"); | |
dst.currency = unit.shift(); | |
dst.issuer = unit.shift(); | |
dst.unit = counter; | |
if (ispair(src, dst)) | |
list.push(pair); | |
} | |
} | |
return list; | |
} | |
function setlines(error, response) | |
{ | |
var lines, i; | |
debug(error, response); | |
if (error) { | |
console.log("Failed to get lines"); | |
return start(); | |
} | |
lines = response.lines; | |
for (i = 0; i < lines.length; i++) { | |
var line = lines[i]; | |
var balance = parseFloat(line.balance); | |
var active = line.no_ripple; | |
var currency = line.currency; | |
var account = line.account; | |
var unit = currency.concat(":", account); | |
if (active) | |
saldo[unit] = balance; | |
} | |
pairs = getpairs(); | |
npairs = pairs.length; | |
minstake = 2 * Math.sqrt(npairs * maxfee / saldo["XRP"]); | |
if (minstake < 0.0032) | |
minstake = 0.0032; | |
maxstake = 0.0128; | |
if (maxstake < minstake) | |
maxstake = minstake; | |
debug(pairs, npairs, minstake, maxstake); | |
remote.request_account_offers(id, ledger, check); | |
} | |
function getunit(amount) | |
{ | |
if ("string" == typeof amount) | |
return "XRP"; | |
return amount.currency.concat(":", amount.issuer); | |
} | |
function getvalue(amount) | |
{ | |
if ("object" == typeof amount) | |
return parseFloat(amount.value); | |
else | |
return parseFloat(amount) / 1e6; | |
} | |
function parse(offers) | |
{ | |
var dict = {}; | |
var i; | |
for (i = 0; i < offers.length; i++) { | |
var offer = offers[i]; | |
var src = offer.taker_gets; | |
var dst = offer.taker_pays; | |
var base = getunit(src); | |
var counter = getunit(dst); | |
var pair = base.concat(">", counter); | |
dict[pair] = { | |
seq: offer.seq, | |
src: getvalue(src), | |
dst: getvalue(dst), | |
dup: dict[pair] | |
}; | |
} | |
return dict; | |
} | |
function product() | |
{ | |
var value = 1; | |
var unit; | |
for (unit in saldo) | |
value *= saldo[unit]; | |
return value; | |
} | |
function profit() | |
{ | |
var prev = state[state.length - 1]; | |
var last = product(); | |
if (prev) | |
prev = prev.value; | |
else | |
prev = 0; | |
return (prev < last); | |
} | |
function rate(step) | |
{ | |
var src = state[step - 1]; | |
var dst = state[step]; | |
var t1, t0, v1, v0; | |
var dt, dv; | |
if (src) { | |
t0 = src.time; | |
v0 = src.value; | |
} else { | |
t0 = 0; | |
v0 = 0; | |
} | |
if (dst) { | |
t1 = dst.time; | |
v1 = dst.value; | |
} else { | |
t1 = now(); | |
v1 = product(); | |
} | |
dt = t1 - t0; | |
dv = v1 - v0; | |
if (dv < 0) | |
dv = 0; | |
return dv / dt; | |
} | |
function ema(last) | |
{ | |
var avg = rate(0); | |
var i; | |
for (i = 1; i <= last; i++) | |
avg = (rate(i) + avg) / 2; | |
return avg; | |
} | |
function getstake(step) | |
{ | |
var entry = state[step - 1]; | |
if (entry) | |
return entry.stake; | |
else | |
return minstake; | |
} | |
function optimize() | |
{ | |
var interest = ema(state.length) / product(); | |
var optimum; | |
if (profit()) | |
optimum = Math.sqrt(timeout * interest); | |
else | |
optimum = getstake(state.length); | |
debug(timeout, interest, optimum); | |
if (!optimum) | |
optimum = minstake; | |
if (optimum < minstake) | |
optimum = minstake; | |
if (maxstake < optimum) | |
optimum = maxstake; | |
return optimum; | |
} | |
function compute(offers) | |
{ | |
var dict = {}; | |
var i; | |
stake = optimize(); | |
for (i = 0; i < npairs; i++) { | |
var pair = pairs[i]; | |
var units = pair.split(">"); | |
var base = units.shift(); | |
var counter = units.shift(); | |
var src = saldo[base]; | |
var dst = saldo[counter]; | |
var prev = offers[pair]; | |
dict[pair] = { | |
src: stake * src / (1 + stake), | |
dst: stake * dst / (1 - stake), | |
seq: prev ? prev.seq : undefined | |
}; | |
} | |
return dict; | |
} | |
function round(value) | |
{ | |
if (value < 1e6) | |
return value.toPrecision(6); | |
else | |
return value.toFixed(0); | |
} | |
function diff(offer, old) | |
{ | |
var p0, p1; | |
if (!old) | |
return 1; | |
p0 = old.src / old.dst; | |
p1 = offer.src / offer.dst; | |
return Math.abs(p1 - p0) / p0; | |
} | |
function worth(offer, pair, fee) | |
{ | |
var src, dst, base, counter, v0, v1; | |
if (!offer) | |
return 0; | |
src = offer.src; | |
dst = offer.dst; | |
pair = pair.split(">"); | |
base = pair.shift(); | |
base = saldo[base]; | |
counter = pair.shift(); | |
counter = saldo[counter]; | |
v0 = base * counter; | |
v1 = (base - src) * (counter + dst); | |
if (fee) | |
v1 *= 1 - npairs * maxfee / saldo["XRP"]; | |
return (v1 - v0) / v0; | |
} | |
function check(error, response) | |
{ | |
var selected = {}; | |
var computed, existing, pair, update; | |
debug(error, response); | |
if (error) { | |
console.log("Failed to get offers"); | |
return start(); | |
} | |
existing = parse(response.offers); | |
computed = compute(existing); | |
for (pair in computed) { | |
var offer = computed[pair]; | |
var old = existing[pair]; | |
var delta = diff(offer, old); | |
var w0 = worth(old, pair, false); | |
var w1 = worth(offer, pair, true); | |
debug(delta, w1, w0, pair); | |
if ((stake < delta) || (w0 < w1)) { | |
selected[pair] = offer; | |
update = true; | |
} | |
} | |
if (update) { | |
if (!profit()) { | |
console.log("Bug"); | |
bug = true; | |
} | |
return reset(selected); | |
} | |
console.log("Offers still exist"); | |
finish(); | |
} | |
function reset(offers) | |
{ | |
var time = now(); | |
var last = state[state.length - 1]; | |
var list = env.RIPPLE_LIST; | |
var unit, pair, i; | |
if (bug && last && last.bugs) { | |
last.value = product(); | |
last.stake = stake; | |
last.bugs.push(time); | |
} else { | |
var entry = { | |
time: time, | |
value: product(), | |
stake: stake, | |
bugs: [] | |
}; | |
state.push(entry); | |
} | |
save(); | |
console.log("Time", time); | |
console.log("Stake", round(stake)); | |
if (list) { | |
var selected = {}; | |
list = list.split(","); | |
for (i = 0; i < list.length; i++) { | |
var unit = list[i]; | |
selected[unit] = saldo[unit]; | |
} | |
saldo = selected; | |
} | |
for (unit in saldo) { | |
var balance = round(saldo[unit]); | |
console.log("Balance", balance, unit); | |
} | |
for (pair in offers) | |
create(offers[pair], pair); | |
for (i = 0; i < queue.length; i++) { | |
var offer = queue[i]; | |
console.log("Create", offer.label); | |
offer.submit(hedge); | |
} | |
if (!i) | |
finish(); | |
} | |
function hedge(error, response) | |
{ | |
debug(error, response); | |
++count; | |
if (queue.length <= count) { | |
console.log("Offers submitted"); | |
finish(); | |
} | |
} | |
function amount(value, unit) | |
{ | |
var currency, issuer; | |
unit = unit.split(":"); | |
currency = unit.shift(); | |
value = round(value); | |
value = value.concat(" ", currency); | |
value = ripple.Amount.from_human(value); | |
issuer = unit.shift(); | |
if (issuer) | |
value.set_issuer(issuer); | |
return value; | |
} | |
function create(offer, pair) | |
{ | |
var transaction = remote.transaction(); | |
var src = offer.src; | |
var dst = offer.dst; | |
var seq = offer.seq; | |
var price = (src < dst ? dst / src : src / dst); | |
var base, counter; | |
price = round(price); | |
transaction.label = price.concat(" ", pair); | |
pair = pair.split(">"); | |
base = pair.shift(); | |
counter = pair.shift(); | |
src = amount(src, base); | |
dst = amount(dst, counter); | |
transaction.offer_create(id, dst, src, false, seq); | |
transaction.set_flags("Sell"); | |
queue.push(transaction); | |
} | |
function giveup() | |
{ | |
console.log("Give up"); | |
finish(); | |
} | |
function finish() | |
{ | |
process.exit(); | |
} | |
state = load(); | |
setTimeout(giveup, timeout); | |
remote.set_secret(id, env.RIPPLE_KEY); | |
remote.connect(start); |
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
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Statistics</title> | |
<link href="http://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.css" | |
rel="stylesheet"> | |
<script src="http://code.jquery.com/jquery.js"></script> | |
<script src="http://code.highcharts.com/stock/highstock.js"></script> | |
<script> | |
var table, template, state, series, cutoff; | |
function now() | |
{ | |
var date = new Date(); | |
return date.getTime(); | |
} | |
function rate(step) | |
{ | |
var src = state[step - 1]; | |
var dst = state[step]; | |
var t1, t0, v1, v0; | |
var dt, dv; | |
if (src) { | |
t0 = src.time; | |
v0 = src.value; | |
} else { | |
t0 = 0; | |
v0 = 0; | |
} | |
if (dst) { | |
t1 = dst.time; | |
v1 = dst.value; | |
} else { | |
t1 = now(); | |
v1 = product(); | |
} | |
dt = t1 - t0; | |
dv = v1 - v0; | |
if (dv < 0) | |
dv = 0; | |
return dv / dt; | |
} | |
function interest(step) | |
{ | |
var src = state[cutoff]; | |
var dst = state[step]; | |
var year = 60 * 24 * 365.25; | |
if (!src || !dst || (cutoff == step)) | |
return 0; | |
ratio = dst.value / src.value; | |
period = dst.time - src.time; | |
return Math.pow(ratio, year / period) - 1; | |
} | |
function addrow(change, step) | |
{ | |
var row = template.clone(); | |
var children = row.children(); | |
function fill() | |
{ | |
var field = $(this).attr("headers"); | |
var datum = step[field]; | |
if (datum) | |
$(this).text(datum); | |
} | |
children.children("span." + change).removeClass("invisible"); | |
children.each(fill); | |
table.prepend(row); | |
} | |
function load(data) | |
{ | |
var date = new Date(); | |
var timezone = date.getTimezoneOffset(); | |
var i, deposit; | |
state = data; | |
if (state.length) | |
deposit = state[0].value; | |
series = [{ | |
yAxis: 0, | |
name: $("th#stake").text(), | |
type: "line", | |
step: "step", | |
tooltip: { | |
valueDecimals: 2, | |
valueSuffix: "%" | |
}, | |
data: [] | |
}, { | |
yAxis: 1, | |
name: $("th#rate").text(), | |
type: "spline", | |
marker: { | |
enabled: true, | |
radius: 4 | |
}, | |
tooltip: { | |
valueDecimals: 2, | |
valueSuffix: "\u2031" | |
}, | |
data: [] | |
}, { | |
yAxis: 2, | |
name: $("th#growth").text(), | |
type: "column", | |
tooltip: { | |
valueDecimals: 2, | |
valueSuffix: "\u2031" | |
}, | |
data: [] | |
}]; | |
table = $("tbody"); | |
template = $("tr.template").detach(); | |
template.removeClass("template"); | |
cutoff = location.search; | |
cutoff = cutoff.replace("?cutoff=", ""); | |
cutoff = parseInt(cutoff); | |
if (!cutoff) | |
cutoff = 0; | |
for (i = 0; i < state.length; i++) { | |
var prev = state[i - 1]; | |
var last = state[i]; | |
var time = last.time; | |
var bugs = last.bugs; | |
var change, j; | |
if (!prev) | |
prev = last; | |
last.value *= 100 / deposit; | |
last.date = new Date(last.time); | |
last.time /= 6e4; | |
last.stake *= 100; | |
last.abs = 100 * rate(i); | |
last.rate = i ? (last.abs + prev.rate) / 2 : last.abs; | |
last.interest = 100 * interest(i); | |
last.since = last.time - prev.time; | |
last.growth = 100 * (last.value - prev.value); | |
if (i < cutoff) | |
continue; | |
if (last.value <= prev.value) | |
change = "stalled"; | |
else if (last.rate < prev.rate) | |
change = "worse"; | |
else | |
change = "better"; | |
addrow(change, { | |
index: i.toFixed(0), | |
growth: last.growth.toFixed(2) + "\u2031", | |
since: last.since.toFixed(0) + " min", | |
value: last.value.toFixed(2) + "%", | |
stake: last.stake.toFixed(2) + "%", | |
date: last.date.toLocaleDateString(), | |
time: last.date.toLocaleTimeString(), | |
rate: last.rate.toFixed(2) + "\u2031", | |
interest: last.interest.toFixed(2) + "%" | |
}); | |
series[0].data.push([time, last.stake]); | |
series[1].data.push([time, last.rate]); | |
series[2].data.push([time, last.growth]); | |
if (!bugs) | |
continue; | |
for (j = 0; j < bugs.length; j++) { | |
var bug = new Date(bugs[j]); | |
addrow("bug", { | |
date: bug.toLocaleDateString(), | |
time: bug.toLocaleTimeString() | |
}); | |
} | |
} | |
Highcharts.setOptions({ | |
global: { | |
timezoneOffset: timezone | |
} | |
}); | |
$("div.chart").highcharts("StockChart", { | |
tooltip: { | |
animation: false, | |
hideDelay: 0 | |
}, | |
plotOptions: { | |
series: { | |
startOnTick: true, | |
endOnTick: true, | |
dataGrouping: { | |
smoothed: true, | |
forced: true | |
} | |
} | |
}, | |
xAxis: { | |
minTickInterval: 6e4, | |
ordinal: false | |
}, | |
chart: { | |
animation: false, | |
height: 480 | |
}, | |
yAxis: [{ | |
showLastLabel: true, | |
startOnTick: false, | |
endOnTick: false, | |
height: "30%" | |
}, { | |
showLastLabel: true, | |
startOnTick: false, | |
endOnTick: false, | |
labels: { | |
align: "right", | |
x: 0 | |
}, | |
top: "35%", | |
height: "30%" | |
}, { | |
showLastLabel: true, | |
startOnTick: false, | |
endOnTick: false, | |
labels: { | |
align: "right", | |
x: 0 | |
}, | |
top: "70%", | |
height: "30%", | |
offset: 0 | |
}], | |
rangeSelector: { | |
buttons: [{ | |
type: "hour", | |
count: 3, | |
text: "3h" | |
}, { | |
type: "hour", | |
count: 8, | |
text: "8h" | |
}, { | |
type: "day", | |
count: 1, | |
text: "1d" | |
}, { | |
type: "day", | |
count: 3, | |
text: "3d" | |
}, { | |
type: "week", | |
count: 1, | |
text: "1w" | |
}, { | |
type: "month", | |
count: 1, | |
text: "1m" | |
}, { | |
type: "month", | |
count: 3, | |
text: "3m" | |
}, { | |
type: "all", | |
text: "All" | |
}], | |
selected: 0 | |
}, | |
navigator: { | |
baseSeries: 1 | |
}, | |
series: series | |
}); | |
} | |
function main() | |
{ | |
$.getJSON("state.json", load); | |
} | |
$(main); | |
</script> | |
</head> | |
<body> | |
<div class="container-fluid"> | |
<div class="chart page-header"></div> | |
<table class="table table-striped table-bordered text-right"> | |
<thead> | |
<tr> | |
<th id="index">#</th> | |
<th id="date">Date</th> | |
<th id="time">Time</th> | |
<th id="since">Passed</th> | |
<th id="change">Change</th> | |
<th id="stake">Stake</th> | |
<th id="rate">Rate</th> | |
<th id="growth">Growth</th> | |
<th id="value">Value</th> | |
<th id="interest">Annual</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr class="template"> | |
<td headers="index"></td> | |
<td headers="date"></td> | |
<td headers="time"></td> | |
<td headers="since"></td> | |
<td headers="change" class="text-center"> | |
<span class="invisible bug label label-default">Bug</span> | |
<span class="invisible stalled label label-danger">Stalled</span> | |
<span class="invisible worse label label-warning">Worse</span> | |
<span class="invisible better label label-success">Better</span> | |
</td> | |
<td headers="stake"></td> | |
<td headers="rate"></td> | |
<td headers="growth"></td> | |
<td headers="value"></td> | |
<td headers="interest"></td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</body> | |
</html> |
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
RIPPLE_LIB = node_modules/ripple-lib/package.json | |
all: hedge.js $(RIPPLE_LIB) | |
node hedge | tee latest.log | tee -a full.log | |
if grep -q "Offers submitted" latest.log; then \ | |
$(MAKE) email; \ | |
$(MAKE) trips; \ | |
fi | |
$(RIPPLE_LIB): | |
npm install ripple-lib@0.9.1 | |
init: | |
-rm -i state.json | |
$(MAKE) | |
email: latest.log | |
awk '/^Create / {print $$2}' $< | sort -n | mailx -s \ | |
"`awk '/^Balance / {ORS=" "; print $$2}' $<`" \ | |
$$LOGNAME | |
loop: | |
-while true; do \ | |
$(MAKE); \ | |
sleep 60; \ | |
done | |
trips: trips.awk full.log | |
awk -f trips.awk full.log | tee profit.log | |
touch old.log | |
diff old.log profit.log >update.log || mailx -s \ | |
"trips change" $$LOGNAME <update.log | |
cp profit.log old.log | |
daemon: stop | |
nohup $(MAKE) loop >/dev/null 2>&1 & echo $$! >daemon.pid | |
sleep 1 | |
stop: | |
-if [ -f daemon.pid ]; then \ | |
kill "`cat daemon.pid`"; \ | |
rm -f daemon.pid; \ | |
fi | |
clean: stop | |
-rm -i state.json *.log | |
-rm -fr node_modules |
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
function mark(label, step) | |
{ | |
for (unit = 1; unit <= nunits; unit++) | |
label = label "\t" saldo[step, unit]; | |
print label; | |
} | |
function profit(src, dst) | |
{ | |
growth = 0; | |
for (unit = 1; unit <= nunits; unit++) { | |
srcsaldo = saldo[src, unit]; | |
dstsaldo = saldo[dst, unit]; | |
if (dstsaldo < srcsaldo) | |
return 0; | |
if (srcsaldo < dstsaldo) | |
growth = 1; | |
} | |
if (growth) { | |
mark("---", src); | |
mark("+++", dst); | |
} | |
return growth; | |
} | |
"Balance" == $1 && "XRP" == $3 { | |
++nsteps; | |
unit = 1; | |
saldo[nsteps, unit] = $2; | |
} | |
"Balance" == $1 && "XRP" != $3 { | |
++unit; | |
if (nunits < unit) | |
nunits = unit; | |
saldo[nsteps, unit] = $2; | |
} | |
END { | |
for (src = 1; src <= nsteps; src++) | |
for (dst = nsteps; src < dst; dst--) | |
if (profit(src, dst)) | |
src = dst; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment