mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-20 13:37:23 +02:00
Script: Add bond-calculator.py
Add a new helper module to calculate fidelity bonds values and stats, and a script to use it. The goal is to give as much information as possible to the user before committing to a fidelity bond.
This commit is contained in:
parent
bbd5019620
commit
72bf4474dd
5 changed files with 328 additions and 1 deletions
|
|
@ -208,7 +208,41 @@ 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).
|
||||
|
||||
At any time you can use the orderbook watcher script to see your own fidelity bond value.
|
||||
To see how valuable a bond would be, and to compare it with the orderbook, you can use the `bond-calculator.py` script.
|
||||
|
||||
For example (see `-h` option for more):
|
||||
|
||||
```
|
||||
(jmvenv) $ python3 scripts/bond-calculator.py -o /home/user/Downloads/orderbook.json 1btc
|
||||
User data location: /home/user/.joinmarket/
|
||||
Amount locked: 100000000 (1.00000000 btc)
|
||||
Confirmation time: 2022-06-16 17:29:26.849274
|
||||
Interest rate: 0.015 (1.5%)
|
||||
Exponent: 1.3
|
||||
|
||||
FIDELITY BOND VALUES (BTC^1.3)
|
||||
|
||||
See /docs/fidelity-bonds.md for complete formula and more
|
||||
|
||||
Locktime: 2022-7
|
||||
Bond value: 0.0000000001579529
|
||||
Weight: 0.00001 (0.00% of all bonds)
|
||||
Top 85% of the orderbook by value
|
||||
|
||||
Locktime: 2022-8
|
||||
Bond value: 0.0000000007090171
|
||||
Weight: 0.00005 (0.00% of all bonds)
|
||||
Top 64% of the orderbook by value
|
||||
|
||||
Locktime: 2022-9
|
||||
Bond value: 0.0000000013980293
|
||||
Weight: 0.00010 (0.01% of all bonds)
|
||||
Top 59% of the orderbook by value
|
||||
|
||||
[...snipped...]
|
||||
```
|
||||
|
||||
At any time you can use the [orderbook watcher](orderbook.md) script to see your own fidelity bond value, and to download the orderbook in JSON format (`Export orders`).
|
||||
|
||||
Consider also the [warning on the bitcoin wiki page on timelocks](https://en.bitcoin.it/wiki/Timelock#Far-future_locks).
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ from .payjoin import (parse_payjoin_setup, send_payjoin,
|
|||
from .websocketserver import JmwalletdWebSocketServerFactory, \
|
||||
JmwalletdWebSocketServerProtocol
|
||||
from .wallet_rpc import JMWalletDaemon
|
||||
from .bond_calc import get_bond_values
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
|
||||
try:
|
||||
|
|
|
|||
87
jmclient/jmclient/bond_calc.py
Normal file
87
jmclient/jmclient/bond_calc.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
Utilities to calculate fidelity bonds values and statistics.
|
||||
"""
|
||||
from bisect import bisect_left
|
||||
from datetime import datetime
|
||||
from statistics import quantiles
|
||||
from typing import Optional, Dict, Any, Mapping, Tuple, List
|
||||
|
||||
from jmclient import FidelityBondMixin, jm_single, get_interest_rate
|
||||
|
||||
|
||||
def get_next_locktime(dt: datetime) -> datetime:
|
||||
"""
|
||||
Return the next valid fidelity bond locktime.
|
||||
"""
|
||||
year = dt.year + dt.month // 12
|
||||
month = dt.month % 12 + 1
|
||||
return datetime(year, month, 1)
|
||||
|
||||
|
||||
def get_bond_values(amount: int,
|
||||
months: int,
|
||||
confirm_time: Optional[float] = None,
|
||||
interest: Optional[float] = None,
|
||||
exponent: Optional[float] = None,
|
||||
orderbook: Optional[Mapping[str, Any]] = None) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
"""
|
||||
Conveniently generate values [and statistics] for multiple possible fidelity bonds.
|
||||
|
||||
Args:
|
||||
amount: Fidelity bond UTXO amount in satoshi
|
||||
months: For how many months to calculate the results
|
||||
confirm_time: Fidelity bond UTXO confirmation time as timestamp, if None, current time is used.
|
||||
I.e., like if the fidelity bond UTXO with given amount has just confirmed on the blockchain.
|
||||
interest: Interest rate, if None, value is taken from config
|
||||
exponent: Exponent, if None, value is taken from config
|
||||
orderbook: Orderbook data, if given, additional statistics are included in the results.
|
||||
Returns:
|
||||
A tuple with 2 elements.
|
||||
First is a dictionary with all the parameters used to perform fidelity bond calculations.
|
||||
Second is a list of dictionaries, one for each month, with the results.
|
||||
"""
|
||||
current_time = datetime.now().timestamp()
|
||||
if confirm_time is None:
|
||||
confirm_time = current_time
|
||||
if interest is None:
|
||||
interest = get_interest_rate()
|
||||
if exponent is None:
|
||||
exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
|
||||
use_config_exp = True
|
||||
else:
|
||||
old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
|
||||
jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
|
||||
use_config_exp = False
|
||||
if orderbook:
|
||||
bond_values = [fb["bond_value"] for fb in orderbook["fidelitybonds"]]
|
||||
bonds_sum = sum(bond_values)
|
||||
percentiles = quantiles(bond_values, n=100, method="inclusive")
|
||||
|
||||
parameters = {
|
||||
"amount": amount,
|
||||
"confirm_time": confirm_time,
|
||||
"current_time": current_time,
|
||||
"interest": interest,
|
||||
"exponent": exponent,
|
||||
}
|
||||
locktime = get_next_locktime(datetime.fromtimestamp(current_time))
|
||||
results = []
|
||||
for _ in range(months):
|
||||
fb_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
||||
amount,
|
||||
confirm_time,
|
||||
locktime.timestamp(),
|
||||
current_time,
|
||||
interest,
|
||||
)
|
||||
result = {"locktime": locktime.timestamp(),
|
||||
"value": fb_value}
|
||||
if orderbook:
|
||||
result["weight"] = fb_value / (bonds_sum + fb_value)
|
||||
result["percentile"] = 100 - bisect_left(percentiles, fb_value)
|
||||
results.append(result)
|
||||
locktime = get_next_locktime(locktime)
|
||||
if not use_config_exp:
|
||||
# We don't want the modified exponent value to persist in memory, so we reset to whatever it was before
|
||||
jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
|
||||
return parameters, results
|
||||
73
jmclient/test/test_bond_calc.py
Normal file
73
jmclient/test/test_bond_calc.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from jmclient import jm_single, load_test_config, FidelityBondMixin
|
||||
from jmclient.bond_calc import get_next_locktime, get_bond_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(('date', 'next_locktime'),
|
||||
((datetime(2022, 1, 1, 1, 1), datetime(2022, 2, 1)),
|
||||
(datetime(2022, 11, 1, 1, 1), datetime(2022, 12, 1)),
|
||||
(datetime(2022, 12, 1, 1, 1), datetime(2023, 1, 1))))
|
||||
def test_get_next_locktime(date: datetime, next_locktime: datetime) -> None:
|
||||
assert get_next_locktime(date) == next_locktime
|
||||
|
||||
|
||||
def test_get_bond_values() -> None:
|
||||
load_test_config()
|
||||
# 1 BTC
|
||||
amount = pow(10, 8)
|
||||
months = 1
|
||||
interest = jm_single().config.getfloat("POLICY", "interest_rate")
|
||||
exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
|
||||
parameters, results = get_bond_values(amount, months)
|
||||
assert parameters["amount"] == amount
|
||||
assert parameters["current_time"] == parameters["confirm_time"]
|
||||
assert parameters["interest"] == interest
|
||||
assert parameters["exponent"] == exponent
|
||||
assert len(results) == months
|
||||
locktime = datetime.fromtimestamp(results[0]["locktime"])
|
||||
assert locktime.month == get_next_locktime(datetime.now()).month
|
||||
value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
||||
parameters["amount"],
|
||||
parameters["confirm_time"],
|
||||
results[0]["locktime"],
|
||||
parameters["current_time"],
|
||||
parameters["interest"],
|
||||
)
|
||||
assert results[0]["value"] == value
|
||||
|
||||
months = 12
|
||||
interest = 0.02
|
||||
exponent = 2
|
||||
confirm_time = datetime(2021, 12, 1).timestamp()
|
||||
parameters, results = get_bond_values(amount,
|
||||
months,
|
||||
confirm_time,
|
||||
interest,
|
||||
exponent)
|
||||
assert parameters["amount"] == amount
|
||||
assert parameters["current_time"] != parameters["confirm_time"]
|
||||
assert parameters["confirm_time"] == confirm_time
|
||||
assert parameters["interest"] == interest
|
||||
assert parameters["exponent"] == exponent
|
||||
assert len(results) == months
|
||||
current_time = datetime.now()
|
||||
# get_bond_values(), at the end, reset the exponent to the config one.
|
||||
# So we have to set the exponent here, otherwise the bond value calculation
|
||||
# won't match and the assert would fail.
|
||||
old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
|
||||
jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
|
||||
for result in results:
|
||||
locktime = datetime.fromtimestamp(result["locktime"])
|
||||
assert locktime.month == get_next_locktime(current_time).month
|
||||
current_time = locktime
|
||||
value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
|
||||
parameters["amount"],
|
||||
parameters["confirm_time"],
|
||||
result["locktime"],
|
||||
parameters["current_time"],
|
||||
parameters["interest"],
|
||||
)
|
||||
assert result["value"] == value
|
||||
jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
|
||||
132
scripts/bond-calculator.py
Executable file
132
scripts/bond-calculator.py
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from json import loads
|
||||
from optparse import OptionParser
|
||||
|
||||
from jmbase import EXIT_ARGERROR, jmprint, get_log, utxostr_to_utxo, EXIT_FAILURE
|
||||
from jmbitcoin import amount_to_sat, sat_to_btc
|
||||
from jmclient import add_base_options, load_program_config, jm_single, get_bond_values
|
||||
|
||||
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 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.path_to_json:
|
||||
try:
|
||||
with open(options.path_to_json, "r", encoding="UTF-8") as orderbook:
|
||||
orderbook = loads(orderbook.read())
|
||||
except FileNotFoundError as exc:
|
||||
log.error(exc)
|
||||
sys.exit(EXIT_ARGERROR)
|
||||
else:
|
||||
orderbook = None
|
||||
try:
|
||||
amount = amount_to_sat(args[0])
|
||||
confirm_time = None
|
||||
except ValueError:
|
||||
# If it's not a valid amount then it has to be a UTXO
|
||||
if jm_single().bc_interface is None:
|
||||
log.error("For calculation based on UTXO access to Bitcoin Core is required")
|
||||
sys.exit(EXIT_FAILURE)
|
||||
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, includeconfs=True)[0]
|
||||
amount = utxo_data["value"]
|
||||
if utxo_data["confirms"] == 0:
|
||||
log.warning("Given UTXO is unconfirmed, current time will be used as confirmation time")
|
||||
confirm_time = None
|
||||
elif utxo_data["confirms"] < 0:
|
||||
log.error("Given UTXO is invalid, reason: conflicted")
|
||||
sys.exit(EXIT_ARGERROR)
|
||||
else:
|
||||
current_height = jm_single().bc_interface.get_current_block_height()
|
||||
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)
|
||||
|
||||
parameters, results = get_bond_values(amount,
|
||||
options.months,
|
||||
confirm_time,
|
||||
options.interest,
|
||||
options.exponent,
|
||||
orderbook)
|
||||
jmprint(f"Amount locked: {amount} ({sat_to_btc(amount)} btc)")
|
||||
jmprint(f"Confirmation time: {datetime.fromtimestamp(parameters['confirm_time'])}")
|
||||
jmprint(f"Interest rate: {parameters['interest']} ({parameters['interest'] * 100}%)")
|
||||
jmprint(f"Exponent: {parameters['exponent']}")
|
||||
jmprint(f"\nFIDELITY BOND VALUES (BTC^{parameters['exponent']})")
|
||||
jmprint("\nSee /docs/fidelity-bonds.md for complete formula and more")
|
||||
|
||||
for result in results:
|
||||
locktime = datetime.fromtimestamp(result["locktime"])
|
||||
# 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(result['value']) / Decimal(1e16)):.16f}")
|
||||
if options.path_to_json:
|
||||
jmprint(f"Weight: {result['weight']:.5f} ({result['weight'] * 100:.2f}% of all bonds)")
|
||||
jmprint(f"Top {result['percentile']}% of the orderbook by value")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue