Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/website/docs/concepts/rates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,14 @@ <h2 id="market-makers">Market makers</h2>
<ul>
<li>
<strong>It values what it holds by a curve,</strong> as a core node
does, at the pool's price, but flatter: it gives a little more for
the asset it holds less of.
does, at the market's price on the XRP Ledger, from the pool and the
order book beside it, but flatter: it gives a little more for the
asset it holds less of.
</li>
<li>
<strong>It trades by swapping.</strong> When what a core node's
snapshot promises for a swap beats that valuation by a margin,
after what selling it back on the pool would cost, the market maker
after what selling it back would cost, on the pool or the order book, the market maker
asks its node to swap with that core node, at a rate no worse than
that. Each such swap brings the core node what it lacks.
</li>
Expand Down
7 changes: 5 additions & 2 deletions apps/website/docs/reference/cli/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,8 @@ <h2 id="dassie-market-maker"><code>dassie market-maker</code></h2>
<p>
Trades through core nodes when what their
<a href="/docs/build/snapshots/">snapshots</a> quote beats what it
values its own holdings at, by an AMM pool on the XRP Ledger, and
values its own holdings at, by an AMM pool and the order book on the
XRP Ledger, and
moves your funds back to even on the ledger once they are far from
it. It is not a node: it drives one node of your own, a node
without a public IP in the settlement hubs of both of the pool's
Expand Down Expand Up @@ -703,7 +704,9 @@ <h2 id="dassie-market-maker"><code>dassie market-maker</code></h2>
on, the <code>ownerBook</code> that funds the hub, and a
<code>cap</code>, the most one trade pays of it;
<code>curvePpm</code>, <code>marginPpm</code>,
<code>thresholdPpm</code> and <code>slippagePpm</code>; and, if
<code>thresholdPpm</code> and <code>slippagePpm</code>;
<code>standingOffer</code>, false to keep no standing offer on the
order book; and, if
the host needs them, <code>caFile</code>, roots to trust the XRP
Ledger server by. A config of bands, as releases before the
curve read, is refused, as is one in the format before 0.3.0.
Expand Down
66 changes: 58 additions & 8 deletions apps/website/docs/services/market-maker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ <h2 id="how-it-works">How it works</h2>
quotes, and pushes it to its peers. Your market maker's node peers
with the core nodes it trades through, its <em>targets</em>, and
reads their snapshots. The market maker values its own holdings the
way a core node does, at the price of an XRP Ledger pool between XRP
and a dollar token, but on a flatter curve: it gives a little more
for the asset it holds less of. Every ten seconds it:
way a core node does, at the market's price on the XRP Ledger
between XRP and a dollar token, from its pool and the order book
beside it, but on a flatter curve: it gives a little more for the
asset it holds less of. Every ten seconds it:
</p>
<ol class="steps">
<li>
Expand Down Expand Up @@ -205,7 +206,8 @@ <h2 id="config-and-secrets">Config and secrets</h2>
"curvePpm": 10000,
"marginPpm": 2000,
"thresholdPpm": 50000,
"slippagePpm": 10000
"slippagePpm": 10000,
"standingOffer": true
}</code></pre>
</figure>
<dl class="ref">
Expand Down Expand Up @@ -281,6 +283,15 @@ <h2 id="config-and-secrets">Config and secrets</h2>
fails rather than pays a worse price, in parts per million.
</p>
</dd>
<dt id="standingoffer"><code>standingOffer</code></dt>
<dd>
<p>
Optional, <code>true</code> unless you set it to
<code>false</code>: whether it keeps a
<a href="#the-standing-offer">standing offer</a> on the order
book while it is off even.
</p>
</dd>
<dt id="cafile"><code>caFile</code></dt>
<dd>
<p>
Expand Down Expand Up @@ -338,7 +349,9 @@ <h2 id="run-it">Run it</h2>
<h2 id="curve-and-rebalancing">Its curve and rebalancing</h2>
<p>
The market maker counts what its node holds of each asset in its hub
and on its own account together, and values both at the pool's price.
and on its own account together, and values both at the market's mid:
halfway between the best bid and ask of the pool, its fee counted, and
of the order book beside it, where 100 XRP of offers would fill.
</p>
<ul>
<li>
Expand All @@ -349,9 +362,10 @@ <h2 id="curve-and-rebalancing">Its curve and rebalancing</h2>
</li>
<li>
<strong>The cost of undoing a swap:</strong> what a swap takes
beyond even, the market maker will sell back on the pool one day.
So that part pays the pool's fee and the price impact of a
rebalance, read from the pool when it decides. A swap back toward
beyond even, the market maker will sell back one day, on the pool or
the order book, whichever is cheaper. So that part pays what that
costs: the pool's fee and the price impact of a rebalance, or the
book's spread and depth, read when it decides. A swap back toward
even pays none of it. What is left over is the margin, its profit.
</li>
<li>
Expand All @@ -368,6 +382,42 @@ <h2 id="curve-and-rebalancing">Its curve and rebalancing</h2>
about $75, and a 1% move in XRP's price changes that by 75 cents
either way. A smaller threshold holds less and rebalances more often.
</p>
<h2 id="the-standing-offer">The standing offer</h2>
<p>
Between rebalances, the market maker lets others bring it back to
even, rather than paying the pool's fee itself. While its share of
either asset is more than a fifth of <code>thresholdPpm</code> from
even, one point by default, it keeps one offer on the XRP Ledger's
order book from its node's account: it gives the asset it holds too
much of, as much as takes it back to even, for the other at the
market's mid.
</p>
<ul>
<li>
It replaces the offer, in one transaction, once the mid moves more
than a twentieth of a percent or the amount by more than a tenth,
and before it expires. Each offer expires by itself after five
minutes, so one left behind by a stopped market maker doesn't stand
for long.
</li>
<li>
It takes the offer away once it is back within the band, before a
swap on the pool past the threshold, and when it stops.
</li>
<li>
While it stands, the offer holds the XRP Ledger's owner reserve for
one object, 0.2 XRP, on your node's account. Your node books each
fill as a deposit and a withdrawal on its own.
</li>
</ul>
<p>
On a quiet book, such as the testnet's, few traders take it, and the market
maker mostly rebalances on the pool past its threshold.
</p>
<p class="why">
<a
href="https://github.com/justmoon/dassie/blob/main/spec/decisions/market-maker-order-book.md">Why a standing offer</a>
</p>
<p class="why">
<a href="https://github.com/justmoon/dassie/blob/main/spec/areas/rate-feed.md#market-maker">The market maker in full</a>,
in the spec: what it trusts, and how it counts a swap on its way.
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/architecture/map.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@
"`dassie update`: fetches, verifies and switches to a newer release, and goes back if it does not start",
"`dassie verify-install`, which also names the release key",
"`dassie front-door`: runs a front door, which is no node, and serves names by reading the bootstrap nodes' name lists itself (`DASSIE_BOOTSTRAP_NODES`); `--make-key` makes its key and prints the entry a node names it by",
"`dassie market-maker`: with one periphery node of its own in two hubs, pinned to one or more core nodes, values its holdings by a curve flatter than the core nodes', at an AMM pool's price on the XRP Ledger, and swaps through every core node whose snapshot beats that by a margin after what undoing the trade on the pool would cost, several at once, asking its node for no less than the rate that clears; once its share of an asset is far from even, it swaps back to even through the pool with a `SendMax` that bounds the price"
"`dassie market-maker`: with one periphery node of its own in two hubs, pinned to one or more core nodes, values its holdings by a curve flatter than the core nodes', at the market's mid on the XRP Ledger, of an AMM pool and the order book beside it, and swaps through every core node whose snapshot beats that by a margin after what undoing the trade on the pool or the book would cost, several at once, asking its node for no less than the rate that clears; while it is a little off even, it keeps one offer of its own on the order book at the mid, which others cross to bring it back; once its share of an asset is far from even, it swaps back to even through the pool with a `SendMax` that bounds the price"
]
},
"node": {
Expand Down
49 changes: 36 additions & 13 deletions crates/dassie-cli/src/market_maker/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
//! steeply it values its inventory, `marginPpm`, what a trade gains after
//! the cost of undoing it, `thresholdPpm`, how far an asset's share may be
//! from even before it rebalances, and `slippagePpm`, how far the pool's
//! price may move against a rebalance. `caFile`
//! price may move against a rebalance. `standingOffer`, true unless it is
//! false, has it keep a standing offer on the order book while it is off
//! even (spec/decisions/market-maker-order-book.md). `caFile`
//! adds roots to trust the XRPL server by, besides the platform's, and
//! `allowPrivate` lets it be on a private address, and on `ws://`: both
//! for dev and tests, whose hosts may have no roots of their own. Every problem is a [`ConfigError`]; nothing
Expand Down Expand Up @@ -200,6 +202,7 @@ pub fn parse(text: &str) -> Result<Config, ConfigError> {
"marginPpm",
"thresholdPpm",
"slippagePpm",
"standingOffer",
"caFile",
"allowPrivate",
],
Expand Down Expand Up @@ -230,18 +233,7 @@ pub fn parse(text: &str) -> Result<Config, ConfigError> {
if xrp.owner_book == token_ledgers.owner_book {
return Err(ConfigError::SameLedger("owner book"));
}
let threshold_ppm = root.ppm("thresholdPpm", MAX_THRESHOLD_PPM)?;
if threshold_ppm == 0 {
return Err(root.invalid("thresholdPpm"));
}
let params = Params {
curve_ppm: root.ppm("curvePpm", MAX_CURVE_PPM)?,
margin_ppm: root.ppm("marginPpm", MAX_MARGIN_PPM)?,
threshold_ppm,
slippage_ppm: root.ppm("slippagePpm", MAX_SLIPPAGE_PPM)?,
xrp_cap,
token_cap,
};
let params = params(&root, xrp_cap, token_cap)?;
Ok(Config {
socket,
passphrase_file,
Expand All @@ -255,6 +247,30 @@ pub fn parse(text: &str) -> Result<Config, ConfigError> {
})
}

/// The strategy's settings, with the caps of the two assets.
fn params(root: &Fields<'_>, xrp_cap: u64, token_cap: u64) -> Result<Params, ConfigError> {
let threshold_ppm = root.ppm("thresholdPpm", MAX_THRESHOLD_PPM)?;
if threshold_ppm == 0 {
return Err(root.invalid("thresholdPpm"));
}
// On unless the config turns it off (spec/decisions/market-maker-order-book.md).
let standing_offer = match root.get("standingOffer") {
None => true,
Some(value) => value
.as_bool()
.ok_or_else(|| root.invalid("standingOffer"))?,
};
Ok(Params {
curve_ppm: root.ppm("curvePpm", MAX_CURVE_PPM)?,
margin_ppm: root.ppm("marginPpm", MAX_MARGIN_PPM)?,
threshold_ppm,
slippage_ppm: root.ppm("slippagePpm", MAX_SLIPPAGE_PPM)?,
xrp_cap,
token_cap,
standing_offer,
})
}

/// An object's fields, with its path for errors.
struct Fields<'a> {
map: &'a Map<String, Value>,
Expand Down Expand Up @@ -503,6 +519,13 @@ mod tests {
assert_eq!(config.params.threshold_ppm, 50_000);
assert_eq!(config.params.token_cap, 500_000_000);
assert_eq!(config.params.cap(Side::Xrp), 1_000_000_000);
assert!(config.params.standing_offer);
let off = with(|v| v["standingOffer"] = json!(false)).unwrap();
assert!(!off.params.standing_offer);
assert_eq!(
with(|v| v["standingOffer"] = json!("no")),
Err(ConfigError::Invalid("standingOffer".into()))
);
assert!(!config.allow_private);
assert_eq!(config.ca_file, None);
let config = with(|v| v["caFile"] = json!("/etc/ca.pem")).unwrap();
Expand Down
Loading
Loading