mirror of
https://github.com/aljazceru/ark.git
synced 2026-02-01 01:24:39 +01:00
Add liquidity simulator (#26)
* adds /liquidity-simulator * fix link; add link to simulator
This commit is contained in:
@@ -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:
|
||||
|
||||
4896
website/static/liquidity-simulator/decimal.js
Normal file
4896
website/static/liquidity-simulator/decimal.js
Normal file
File diff suppressed because it is too large
Load Diff
134
website/static/liquidity-simulator/index.html
Normal file
134
website/static/liquidity-simulator/index.html
Normal 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>
|
||||
·
|
||||
<a onclick="dqs('#initialBudget').value=10">10</a>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<a onclick="dqs('#vtxoRatio').value=10">10</a>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<a onclick="dqs('#numberUsers').value=40">40</a>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<a onclick="dqs('#onboardAmount').value=0.1">0.1</a>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<a onclick="dqs('#moneyVelocity').value=0.50">0.50</a>
|
||||
·
|
||||
<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>
|
||||
·
|
||||
<a onclick="dqs('#numberPayments').value=10">10</a>
|
||||
·
|
||||
<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>
|
||||
261
website/static/liquidity-simulator/script.js
Normal file
261
website/static/liquidity-simulator/script.js
Normal 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()
|
||||
}
|
||||
30
website/static/liquidity-simulator/style.css
Normal file
30
website/static/liquidity-simulator/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user