Add liquidity simulator (#26)

* adds /liquidity-simulator

* fix link; add link to simulator
This commit is contained in:
João Bordalo
2023-11-30 18:31:19 +00:00
committed by GitHub
parent f4dee08651
commit 02149a9e70
5 changed files with 5326 additions and 1 deletions

View File

@@ -18,7 +18,7 @@ All transactions within Ark must be funded by the Ark Service Provider (ASP) in
This post discusses some considerations on this topic and calculates the funding needs of the ASP.
Refer to [nomenclature docs](/docs//nomenclature) for any doubt
Refer to [nomenclature docs](/docs/nomenclature) for any doubt
## Ark liquidity requirements
@@ -320,6 +320,10 @@ Dividing the initial UTXO into more VTXOs decreases the need for funding.
:::
## Simulator
You can run your own simulations with the <a href="/liquidity-simulator/" target="_blank">Ark liquidity simulator</a>.
## Conclusion
The liquidity requirements for an ASP will depend on three major factors:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<title>Ark liquidity simulator</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="style.css" />
<body>
<div style="display: flex">
<div style="width: 15%; border-right: 1px solid grey">
<p><strong>ASP</strong></p>
<p>
<label>
Initial budget (BTC)
<input type="number" id="initialBudget" value="1" />
</label>
<br />
<a onclick="dqs('#initialBudget').value=1">1</a>
&middot;
<a onclick="dqs('#initialBudget').value=10">10</a>
&middot;
<a onclick="dqs('#initialBudget').value=100">100</a>
</p>
<p>
<label>
Timelock (days)
<input type="number" id="timelock" value="28" />
</label>
<br />
<a onclick="dqs('#timelock').value=14">2 weeks</a>
&middot;
<a onclick="dqs('#timelock').value=28">1 month</a>
</p>
<p>
<label>
VTXO ratio (1/n)
<input type="number" id="vtxoRatio" value="1" />
</label>
<br />
<a onclick="dqs('#vtxoRatio').value=1">1</a>
&middot;
<a onclick="dqs('#vtxoRatio').value=10">10</a>
&middot;
<a onclick="dqs('#vtxoRatio').value=100">100</a>
</p>
<p>
<label>
Dust limit (sats)
<input type="number" id="dustLimit" value="450" />
</label>
</p>
<p style="margin-top: 3rem"><strong>Users</strong></p>
<p>
<label>
Number of users
<input type="number" id="numberUsers" value="4" />
</label>
<br />
<a onclick="dqs('#numberUsers').value=4">4</a>
&middot;
<a onclick="dqs('#numberUsers').value=40">40</a>
&middot;
<a onclick="dqs('#numberUsers').value=400">400</a>
</p>
<p>
<label>
Onboard amount (BTC)
<input type="number" id="onboardAmount" value="0.01" />
</label>
<br />
<a onclick="dqs('#onboardAmount').value=0.01">0.01</a>
&middot;
<a onclick="dqs('#onboardAmount').value=0.1">0.1</a>
&middot;
<a onclick="dqs('#onboardAmount').value=1">1</a>
</p>
<p>
<label>
Monthly Money Velocity
<input type="number" id="moneyVelocity" value="0.33" />
</label>
<br />
<a onclick="dqs('#moneyVelocity').value=0.33">0.33</a>
&middot;
<a onclick="dqs('#moneyVelocity').value=0.50">0.50</a>
&middot;
<a onclick="dqs('#moneyVelocity').value=1">1</a>
</p>
<p>
<label>
Monthly Payments
<input type="number" id="numberPayments" value="10" />
</label>
<br />
<a onclick="dqs('#numberPayments').value=1">1</a>
&middot;
<a onclick="dqs('#numberPayments').value=10">10</a>
&middot;
<a onclick="dqs('#numberPayments').value=100">100</a>
</p>
</div>
<div style="width: 85%; padding: 0 3rem">
<p><strong>Stats</strong></p>
<div class="graphContainer">
<div>
<p><strong>Payments</strong></p>
<div id="graph1"></div>
</div>
<div>
<p><strong>VTXOs</strong></p>
<div id="graph2"></div>
</div>
<div>
<p><strong>Budget</strong></p>
<div id="graph3"></div>
</div>
</div>
<div class="flexed">
<p><strong>Timeline</strong></p>
<p>
<button id="playButton">Play</button>
<button id="pauseButton">Pause</button>
<button id="resetButton">Reset</button>
</p>
</div>
<p style="margin-top: 0">
Each color tone represents a specific user<br />
Mouse over a circle to see a JSON representation of the payment
</p>
<div class="timelineContainer"></div>
</div>
</div>
</body>
<script src="decimal.js"></script>
<script src="script.js"></script>
</html>

View File

@@ -0,0 +1,261 @@
const dqs = (selector) => document.querySelector(selector)
const clock = {
current: 0,
intervalId: '',
pause: () => clock.removeInterval(),
play() {
const maxDays = Number(dqs('#timelock').value)
// if already running, do nothing
if (clock.intervalId) return
// reset and play timeline if on the end of previous timeline
if (clock.current === maxDays) {
clock.reset()
clock.play()
return
}
// start clock
clock.intervalId = setInterval(() => {
clock.current += 1
if (clock.current === maxDays) clock.removeInterval()
days.db.push(days.randomDay())
ui.render()
}, 300)
},
reset() {
clock.removeInterval()
clock.current = 0
days.db = []
ui.render()
},
removeInterval: () => {
if (clock.intervalId) {
clearInterval(clock.intervalId)
clock.intervalId = ''
}
},
}
const days = {
db: [],
untilNow: () => {
const users = {}
let eachPayment = 0
let eachVTXO = 0
let numEvents = 0
let numVTXOs = 0
let sumPayments = 0
let sumVTXOs = 0
days.db.forEach((day) => {
if (!day) return
day.forEach((event) => {
if (!users[event.user]) users[event.user] = 0
users[event.user] += 1
eachVTXO = event.vtxos.each
eachPayment = event.payment.value
numEvents += 1
numVTXOs = Decimal.add(numVTXOs, event.vtxos.num).toNumber()
sumPayments = Decimal.add(sumPayments, event.payment.value).toNumber()
sumVTXOs = Decimal.add(sumVTXOs, event.vtxos.sum).toNumber()
})
})
return {
eachPayment,
eachVTXO,
numEvents,
numUsers: Object.keys(users).length,
numVTXOs,
ratioVTXOsPayments: Decimal.div(sumVTXOs, sumPayments).toNumber(),
sumPayments,
sumVTXOs,
}
},
randomDay: () => {
let day
const numberUsers = dqs('#numberUsers').value
const onboardAmount = dqs('#onboardAmount').value
const timelock = Number(dqs('#timelock').value)
const moneyVelocity = Decimal.div(timelock, 28)
.mul(dqs('#moneyVelocity').value)
.toNumber()
const numberPayments = Decimal.div(timelock, 28)
.mul(dqs('#numberPayments').value)
.toNumber()
const vtxoRatio = Number(dqs('#vtxoRatio').value)
const paymentValue = Decimal.mul(onboardAmount, moneyVelocity)
.div(numberPayments)
.toNumber()
const vtxoVal = Decimal.div(onboardAmount, vtxoRatio).toNumber()
const vtxoNum = Decimal.div(paymentValue, vtxoVal).ceil().toNumber()
const vtxoSum = Decimal.mul(vtxoNum, vtxoVal).toNumber()
const paymentChange = Decimal.sub(vtxoSum, paymentValue).toNumber()
for (let user = 0; user < numberUsers; user++) {
const rand = timelock * Math.random()
if (rand < numberPayments) {
if (!day) day = []
day.push({
user,
payment: {
value: paymentValue,
change: paymentChange,
},
vtxos: {
each: vtxoVal,
num: vtxoNum,
sum: vtxoSum,
},
})
}
}
return day
},
}
const pretty = {
btc: (num) => pretty.num(num, 0, 8),
num: (num = 0, min = 0, max = 2) => {
if (num === 0) return '0'
return new Intl.NumberFormat('en-us', {
minimumFractionDigits: min,
maximumFractionDigits: max,
}).format(num)
},
}
const ui = {
theme: {
circle: { radius: 5 },
colors: [
'indianred',
'lightCoral',
'red',
'darkred',
'pink',
'hotpink',
'mediumvioletred',
'lightsalmon',
'coral',
'orangered',
'darkorange',
'orange',
'gold',
'moccasin',
'khaki',
'darkkhaki',
'thistle',
'plum',
],
margin: 10,
vSpace: 20,
},
renderStats: () => {
const dustLimit = Decimal.div(
dqs('#dustLimit').value,
100_000_000 // from satoshis to btc
)
const initialBudget = Number(dqs('#initialBudget').value)
const {
eachPayment,
eachVTXO,
numUsers,
numEvents,
numVTXOs,
ratioVTXOsPayments,
sumPayments,
sumVTXOs,
} = days.untilNow()
const connectorsValue = Decimal.mul(numVTXOs, dustLimit).toNumber()
const totalCost = Decimal.add(sumVTXOs, connectorsValue).toNumber()
const budget = {
initial: initialBudget,
payments: sumVTXOs,
connectors: connectorsValue,
available: Decimal.sub(initialBudget, totalCost).toNumber(),
}
const stats = `
<p>Each: ${pretty.btc(eachPayment)} BTC</p>
<p>Number of users: ${numUsers}</p>
<p>Number of events: ${numEvents}</p>
<p>Total payments: ${pretty.btc(sumPayments)} BTC</p>
`
const vtxos = `
<p>Each: ${pretty.btc(eachVTXO)} BTC</p>
<p>VTXOs used: ${numVTXOs}</p>
<p>Total value: ${pretty.btc(sumVTXOs)} BTC</p>
<p>Ratio value / payments: ${pretty.num(ratioVTXOsPayments)}</p>
`
const liquidity = `
<p>Initial: ${pretty.btc(budget.initial)} BTC</p>
<p>Payments: ${pretty.btc(budget.payments)} BTC</p>
<p>Connectors: ${pretty.btc(budget.connectors)} BTC</p>
<p>Available: ${pretty.btc(budget.available)} BTC</p>
`
dqs('#graph1').innerHTML = stats
dqs('#graph2').innerHTML = vtxos
dqs('#graph3').innerHTML = liquidity
},
renderTimeline: () => {
const container = dqs('.timelineContainer')
const containerWidth = container.offsetWidth - ui.theme.margin * 2
const steps = dqs('#timelock').value
const stepWidth = Decimal.div(containerWidth, steps).toNumber()
const height = container.offsetHeight - 2
const width = clock.current * stepWidth + ui.theme.margin * 2
const viewbox = `0 0 ${width} ${height}`
// render event inside timeline
const _event = (event, idx, index) => {
const r = ui.theme.circle.radius
const cx = index * stepWidth + r + ui.theme.margin
const cy = (idx + 2) * ui.theme.vSpace
const fill = ui.theme.colors[event.user % ui.theme.colors.length]
return `
<circle alt="alt" cx="${cx}" cy="${cy}" r="${r}" fill="${fill}">
<title>${JSON.stringify(event, null, 2)}</title>
</circle>
`
}
// render day inside timeline
const _day = (day, index) => {
if (!day) return
const cx = index * stepWidth + ui.theme.circle.radius + ui.theme.margin
const cy = ui.theme.vSpace
const idx = index + 1
const num = `<text text-anchor="middle" x="${cx}" y="${cy}" class="small">${idx}</text>`
return num + day.map((event, idx) => _event(event, idx, index)).join()
}
// render days inside timeline
const _days = () => days.db.map((day, index) => _day(day, index)).join()
// timeline is a svg
const svg = `
<svg viewBox="${viewbox}" height="${height}" width="${width}" xmlns="http://www.w3.org/2000/svg">
<style>
.small {
font: italic 12px sans-serif;
}
</style>
<rect x="0" y="0" width="100%" height="100%" fill="#eee"/>
${_days()}
</svg>
`
dqs('.timelineContainer').innerHTML = svg
},
resizeTimeline: () => {
const container = dqs('.timelineContainer')
const numberUsers = Number(dqs('#numberUsers').value)
const height = (numberUsers + 2) * ui.theme.vSpace
// don't reduce size
if (container?.offsetHeight > height) return
container.style.height = `${height}px`
},
render: () => {
ui.resizeTimeline()
ui.renderTimeline()
ui.renderStats()
},
}
window.onload = () => {
dqs('#pauseButton').onclick = () => clock.pause()
dqs('#playButton').onclick = () => clock.play()
dqs('#resetButton').onclick = () => clock.reset()
}

View File

@@ -0,0 +1,30 @@
input {
margin: 0.21rem 0;
max-width: 90%;
width: 10rem;
}
a {
color: lightCoral;
cursor: pointer;
}
.flexed {
display: flex;
justify-content: space-between;
}
.graphContainer {
column-gap: 3rem;
display: grid;
grid-template-columns: repeat(3, 1fr);
margin-bottom: 3rem;
}
.graphContainer > div {
border: 1px solid grey;
height: 12rem;
padding: 0 1rem;
}
.timelineContainer {
border: 1px solid grey;
text-align: right;
height: 120px;
max-height: 60vh;
}