cmd/llm: modify order acceptance to show breakdown and hide fixed_rate

In tihs commit, we modify the order acceptance commands (llm submit ask
and llm submit bid) to take the final percentage rather than the fixed
rate. Historically we've seen that people find thinking in fixed point
arithmetic difficult. So instead of exposing this value, we now poll the
user for the amount their willing to pay or accept as yield for the
_entire duration_.

With this change, users only need to think about the lump sum coupon
payment rather than the internal fixed rate. This should also lend to
better price discovery as distinct orders may end up with the same
rate_fixed when converted at the end.

In addition to this change, we now show a break down of the order
parameters which includes the execution fee.
This commit is contained in:
Olaoluwa Osuntokun 2020-07-09 19:48:28 -07:00
parent 407a76f6dd
commit 16a77c19df
No known key found for this signature in database
GPG key ID: BC13F65E2DC84465
2 changed files with 178 additions and 28 deletions

View file

@ -233,20 +233,37 @@ channel that can be bought or sold on the network.
With that said, let's place some orders to try to earn some yield from this 0.5
BTC that's been burning a hole in our SD card for the past year. We'll place a
single order for 10 million satoshis, at 0.0001% (the lowest possible price)
for 3000 blocks:
single order for 10 million satoshis, wanting to receive 0.3% (30 bps)
over a 3000 block period (a bit under 3 weeks):
```
🏔 llm orders submit ask 10000000 0288096be9917f8ebdfc6eb2701635fe658f4eae1e0274dcce41418b3fb5145732 --rate_fixed=1 --max_duration_blocks=3000
🏔 llm orders submit ask 10000000 0288096be9917f8ebdfc6eb2701635fe658f4eae1e0274dcce41418b3fb5145732 --interest_rate_percent=0.3 --max_duration_blocks=3000
Ask Amount: 0.1 BTC
Ask Duration: 3000
Total 0.0003 BTC Premium (paid to maker):
Rate Fixed: 1
Rate Per Block: 0.0000010 (0.0001000%)
Execution Fee: 0.00010001 BTC
Confirm order (yes/no): eyes
{
"accepted_order_nonce": "f1bebca6047dee6657f82377ebac94d1dc6667097f2a4d463deb63eff6f0dbcf"
}
```
By leaving off the `--force` flag, we request the final break down to confirm
the details of our order before we put it through.
In this case, if this order is executed, then I'll gain 3k satoshis:
```
3000 = (1/100000)*1000000*3000
```
It's important to note that although internally we use a fixed rate per block
to compute the final premium, on the command line, we accept the final
acceptable premium in a _percent_. Therefore, when submitting orders, one
should place the value that they wish to receive or accept at the end of the
lease period. Internally, we'll then compute the _per block lease rate_ and
submit the order using _that_.
The duration and fixed rate (the percentage) are two important values to pay
attention to when placing orders. Given the same amount, and fixed rate, you
earn more by leasing out the funds for a _longer_ period of time. Conversely, a

View file

@ -1,9 +1,12 @@
package main
import (
"bufio"
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/llm/clmrpc"
@ -14,9 +17,7 @@ import (
const (
defaultFundingFeeRate = chainfee.FeePerKwFloor
defaultAskRate = 5
defaultAskMaxDuration = 210000
defaultBidRate = 5
defaultBidMinDuration = 144
)
@ -51,11 +52,37 @@ var sharedFlags = []cli.Flag{
},
}
// promptForConfirmation continuously prompts the user for the message until
// receiving a response of "yes" or "no" and returns their answer as a bool.
func promptForConfirmation(msg string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(msg)
answer, err := reader.ReadString('\n')
if err != nil {
return false
}
answer = strings.ToLower(strings.TrimSpace(answer))
switch {
case answer == "yes":
return true
case answer == "no":
return false
default:
continue
}
}
}
// parseCommonParams tries to read the common order parameters from the command
// line positional arguments and/or flags and parses them based on their
// destination data type. No formal in-depth validation is performed as the
// server will do that on the RPC level anyway.
func parseCommonParams(ctx *cli.Context) (*clmrpc.Order, error) {
func parseCommonParams(ctx *cli.Context, blockDuration uint32) (*clmrpc.Order, error) {
var (
err error
amt btcutil.Amount
@ -81,7 +108,31 @@ func parseCommonParams(ctx *cli.Context) (*clmrpc.Order, error) {
}
params.FundingFeeRate = ctx.Uint64("funding_fee_rate")
params.RateFixed = uint32(ctx.Uint64("rate_fixed"))
// We'll map the interest rate specified on the command line to our
// internal "rate_fixed" unit.
//
// rate = % / 100
// rate = rateFixed / totalParts
// rateFixed = rate * totalParts
interestPercent := ctx.Float64("interest_rate_percent")
interestRate := interestPercent / 100
rateFixedFloat := interestRate * order.FeeRateTotalParts
// We then take this rate fixed, and divide it by the number of blocks
// as the user wants this rate to be the final lump sum they pay.
rateFixed := uint32(rateFixedFloat / float64(blockDuration))
// At this point, if this value is less than 1, then we aren't able to
// express it given the current precision allowed by our fixed point.
if rateFixed < 1 {
return nil, fmt.Errorf("fixed rate of %v is too small "+
"(%v%% over %v blocks), min is 1 (%v%%)", rateFixed,
interestPercent, blockDuration,
float64(1)/order.FeeRateTotalParts)
}
params.RateFixed = rateFixed
return params, nil
}
@ -117,11 +168,10 @@ var ordersSubmitAskCommand = cli.Command{
Create an offer to provide inbound liquidity to an auction participant
by opening a channel to them for a certain time.`,
Flags: append([]cli.Flag{
cli.Uint64Flag{
Name: "rate_fixed",
Usage: "the rate at which the funds will be lent out " +
"in parts per million",
Value: defaultAskRate,
cli.Float64Flag{
Name: "interest_rate_percent",
Usage: "the total percent one is willing to pay or " +
"accept as yield for the specified interval",
},
cli.Uint64Flag{
Name: "amt",
@ -150,22 +200,44 @@ func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl
return nil
}
params, err := parseCommonParams(ctx)
if err != nil {
return fmt.Errorf("unable to parse order params: %v", err)
}
ask := &clmrpc.Ask{
Details: params,
MaxDurationBlocks: uint32(ctx.Uint64("max_duration_blocks")),
Version: uint32(order.VersionDefault),
}
params, err := parseCommonParams(ctx, ask.MaxDurationBlocks)
if err != nil {
return fmt.Errorf("unable to parse order params: %v", err)
}
ask.Details = params
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
// If the user didn't opt to force submit this order, then we'll show a
// break down of the final order details and request a confirmation
// before we submit.
if !ctx.Bool("force") {
if err := printOrderDetails(
client, btcutil.Amount(ask.Details.Amt),
order.FixedRatePremium(ask.Details.RateFixed),
ask.MaxDurationBlocks, true,
); err != nil {
return fmt.Errorf("unable to print order details: %v", err)
}
if !promptForConfirmation("Confirm order (yes/no): ") {
fmt.Println("Cancelling order...")
return nil
}
return nil
}
resp, err := client.SubmitOrder(
context.Background(), &clmrpc.SubmitOrderRequest{
Details: &clmrpc.SubmitOrderRequest_Ask{
@ -181,6 +253,43 @@ func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl
return nil
}
func printOrderDetails(client clmrpc.TraderClient, amt btcutil.Amount,
rate order.FixedRatePremium, leaseDuration uint32, isAsk bool) error {
auctionFee, err := client.AuctionFee(
context.Background(), &clmrpc.AuctionFeeRequest{},
)
if err != nil {
return err
}
feeSchedule := order.NewLinearFeeSchedule(
btcutil.Amount(auctionFee.ExecutionFee.BaseFee),
btcutil.Amount(auctionFee.ExecutionFee.FeeRate),
)
exeFee := feeSchedule.BaseFee() + feeSchedule.ExecutionFee(amt)
orderType := "Bid"
premiumDescription := "paid to maker"
if isAsk {
orderType = "Ask"
premiumDescription = "yield from taker"
}
ratePerMil := float64(rate) / order.FeeRateTotalParts
premium := rate.LumpSumPremium(amt, leaseDuration)
fmt.Println("-- Order Details --")
fmt.Printf("%v Amount: %v\n", orderType, amt)
fmt.Printf("%v Duration: %v\n", orderType, leaseDuration)
fmt.Printf("Total Premium (%v): %v \n", premiumDescription, premium)
fmt.Printf("Rate Fixed: %v\n", rate)
fmt.Printf("Rate Per Block: %.9f (%.7f%%)\n", ratePerMil, ratePerMil*100)
fmt.Println("Execution Fee: ", exeFee)
return nil
}
var ordersSubmitBidCommand = cli.Command{
Name: "bid",
Usage: "obtain channel liquidity",
@ -190,12 +299,10 @@ var ordersSubmitBidCommand = cli.Command{
Place an offer for acquiring inbound liquidity by lending
funding capacity from another participant in the order book.`,
Flags: append([]cli.Flag{
cli.Uint64Flag{
Name: "rate_fixed",
Usage: "the rate in parts per million that is " +
"acceptable to be paid to the offering " +
"participant for lending the funds out",
Value: defaultBidRate,
cli.Float64Flag{
Name: "interest_rate_percent",
Usage: "the total percent one is willing to pay or " +
"accept as yield for the specified interval",
},
cli.Uint64Flag{
Name: "amt",
@ -213,6 +320,10 @@ var ordersSubmitBidCommand = cli.Command{
"liquidity should be provided for",
Value: defaultBidMinDuration,
},
cli.BoolFlag{
Name: "force",
Usage: "skip order placement confirmation",
},
}, sharedFlags...),
Action: ordersSubmitBid,
}
@ -224,22 +335,44 @@ func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl
return nil
}
params, err := parseCommonParams(ctx)
if err != nil {
return fmt.Errorf("unable to parse order params: %v", err)
}
bid := &clmrpc.Bid{
Details: params,
MinDurationBlocks: uint32(ctx.Uint64("min_duration_blocks")),
Version: uint32(order.VersionDefault),
}
params, err := parseCommonParams(ctx, bid.MinDurationBlocks)
if err != nil {
return fmt.Errorf("unable to parse order params: %v", err)
}
bid.Details = params
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
// If the user didn't opt to force submit this order, then we'll show a
// break down of the final order details and request a confirmation
// before we submit.
if !ctx.Bool("force") {
if err := printOrderDetails(
client, btcutil.Amount(bid.Details.Amt),
order.FixedRatePremium(bid.Details.RateFixed),
bid.MinDurationBlocks, false,
); err != nil {
return fmt.Errorf("unable to print order details: %v", err)
}
if !promptForConfirmation("Confirm order (yes/no): ") {
fmt.Println("Cancelling order...")
return nil
}
return nil
}
resp, err := client.SubmitOrder(
context.Background(), &clmrpc.SubmitOrderRequest{
Details: &clmrpc.SubmitOrderRequest_Bid{