Skip to content

Commit

Permalink
Script: Add bond-calculator.py
Browse files Browse the repository at this point in the history
Add a new script that calculates and shows fidelity bond stats for many possible locktimes.
  • Loading branch information
PulpCattel committed Apr 23, 2022
1 parent 5bf64ed commit 7a825d1
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/fidelity-bonds.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ miner fees, you can probably wait until fees are low).
The full details on valuing a time-locked fidelity bond are [found in the relevant section of the
"Financial mathematics of fidelity bonds" document](https://gist.github.com/chris-belcher/87ebbcbb639686057a389acb9ab3e25b#time-locked-fidelity-bonds).

To see how valuable a bond would be, and to compare it with the orderbook, you can use the `bond-calculator.py` script.

At any time you can use the orderbook watcher script to see your own fidelity bond value.

Consider also the [warning on the bitcoin wiki page on timelocks](https://en.bitcoin.it/wiki/Timelock#Far-future_locks).
Expand Down
144 changes: 144 additions & 0 deletions scripts/bond-calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
import sys
from bisect import bisect_left
from datetime import datetime
from decimal import Decimal
from json import loads
from optparse import OptionParser
from statistics import quantiles, StatisticsError

from dateutil.relativedelta import relativedelta
from jmbase import EXIT_ARGERROR, jmprint, get_log, utxostr_to_utxo
from jmbitcoin import amount_to_sat, sat_to_btc
from jmclient import FidelityBondMixin, add_base_options, load_program_config, jm_single, get_interest_rate

DESCRIPTION = """Given either a Bitcoin UTXO in the form TXID:n
(e.g., 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098:0)
or an amount in either satoshi or bitcoin (e.g., 150000, 0.1, 10.123, 10btc),
calculate fidelity bond values for all possible locktimes in a one-year period
(12 months, you can change that with the `-m --months` option).
By default it uses the values from your joinmarket.cfg,
you can override these with the `-i --interest` and `-e --exponent` options.
Additionally, you can export the orderbook from ob-watcher.py and use the data here
with the `-o --orderbook` option, this will compare the results from this script
with the fidelity bonds in the orderbook.
"""

log = get_log()


def add_one_month(dt: datetime) -> datetime:
return dt + relativedelta(months=1)


def main() -> None:
parser = OptionParser(
usage="usage: %prog [options] UTXO or amount",
description=DESCRIPTION,
)
add_base_options(parser)
parser.add_option(
"-i",
"--interest",
action="store",
type="float",
dest="interest",
help="Interest rate to use for fidelity bond calculation (instead of interest_rate config)",
)
parser.add_option(
"-e",
"--exponent",
action="store",
type="float",
dest="exponent",
help="Exponent to use for fidelity bond calculation (instead of bond_value_exponent config)",
)
parser.add_option(
"-m",
"--months",
action="store",
type="int",
dest="months",
help="For how many months to calculate the fidelity bond values, each month has its own stats (default 12)",
default=12,
)
parser.add_option(
"-o",
"--orderbook",
action="store",
type="str",
dest="path_to_json",
help="Path to the exported orderbook in JSON format",
)

options, args = parser.parse_args()
load_program_config(config_path=options.datadir)
if len(args) != 1:
log.error("Invalid arguments, see --help")
sys.exit(EXIT_ARGERROR)
if options.interest:
jm_single().config.set("POLICY", "interest_rate", str(options.interest))
interest = get_interest_rate()
if options.exponent:
jm_single().config.set("POLICY", "bond_value_exponent", str(options.exponent))
current_time = jm_single().bc_interface.get_best_block_median_time()
current_height = jm_single().bc_interface.get_current_block_height()
if options.path_to_json:
try:
with open(options.path_to_json, "r", encoding="UTF-8") as orderbook:
bond_values = [fb["bond_value"] for fb in loads(orderbook.read())["fidelitybonds"]]
percentiles = quantiles(bond_values, n=100, method="inclusive")
except FileNotFoundError as exc:
log.error(exc)
sys.exit(EXIT_ARGERROR)
except StatisticsError:
log.error("Given JSON file does not contain enough fidelity bonds (at least 2 required)")
sys.exit(EXIT_ARGERROR)

try:
utxo_value = amount_to_sat(args[0])
except ValueError:
# If it's not a valid amount then it has to be a UTXO
success, utxo = utxostr_to_utxo(args[0])
if not success:
# utxo contains the error message
log.error(utxo)
sys.exit(EXIT_ARGERROR)
utxo_data = jm_single().bc_interface.query_utxo_set(utxo, includeconf=True)[0]
utxo_value = utxo_data["value"]
block_hash = jm_single().bc_interface.get_block_hash(current_height - utxo_data["confirms"] + 1)
confirm_time = jm_single().bc_interface.get_block_time(block_hash)
else:
# We assume a fidelity bond with confirmation time equal to the current time (last block median time).
# I.e., like if the fidelity bond UTXO with given amount has just confirmed on the blockchain.
confirm_time = current_time

jmprint(f"Amount locked: {utxo_value} ({sat_to_btc(utxo_value)} btc)")
jmprint(f"Confirmation time: {datetime.fromtimestamp(confirm_time)}")
jmprint(f"Interest rate: {interest} ({interest * 100}%)")
jmprint(f"Exponent: {jm_single().config.get('POLICY', 'bond_value_exponent')}")
jmprint("\nFIDELITY BOND VALUES")

locktime = add_one_month(
datetime.fromtimestamp(current_time).replace(day=1, hour=0, minute=0, second=0, microsecond=0))
for _ in range(options.months):
fb_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
utxo_value,
confirm_time,
datetime.timestamp(locktime),
current_time,
interest,
)
# Mimic the locktime value the user would have to insert to create such fidelity bond
jmprint(f"\nLocktime: {locktime.year}-{locktime.month}")
# Mimic orderbook value
jmprint(f"Bond value: {float(Decimal(fb_value) / Decimal(1e16)):.16f}")
if options.path_to_json:
weight = round(fb_value / sum(bond_values), 5)
jmprint(f"Weight: {weight} ({weight * 100}% of all bonds)")
jmprint(f"Top {100 - bisect_left(percentiles, fb_value)}% of the orderbook by value")
locktime = add_one_month(locktime)


if __name__ == "__main__":
main()

0 comments on commit 7a825d1

Please sign in to comment.