Skip to content

Instantly share code, notes, and snippets.

@codedot
Last active April 16, 2024 07:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save codedot/e2e707ac1ad95f5ac2cd to your computer and use it in GitHub Desktop.
Save codedot/e2e707ac1ad95f5ac2cd to your computer and use it in GitHub Desktop.
Automated multi-currency market maker for Ripple
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);
<!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>
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
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