Skip to content

Instantly share code, notes, and snippets.

@0300dbdd1b
Created November 17, 2023 00:51
Show Gist options
  • Save 0300dbdd1b/49ff763c321a8f9f4f008adb1e3675ba to your computer and use it in GitHub Desktop.
Save 0300dbdd1b/49ff763c321a8f9f4f008adb1e3675ba to your computer and use it in GitHub Desktop.
Experimental fix of UTXOracle V7 ignoring transactions using inputs created on the same day as the transaction.
###############################################################################
# Introduction) This is UTXOracle.py
###############################################################################
# This python program estimates the daily USD price of bitcoin using only
# your bitcoin Core full node. It will work even while you are disconnected
# from the internet because it only reads blocks from your machine. It does not
# save files, write cookies, or access any wallet information. It only reads
# blocks, analyzes output patterns, and estimates a daily average a USD
# price of bitcoin. The call to your node is the standard "bitcoin-cli". The
# date and price ranges expected to work for this version are from 2020-7-26
# and from $10,000 to $100,000
print("UTXOracle version 7.1\n")
###############################################################################
# Quick Start
###############################################################################
# 1. Make sure you have python3 and bitcoin-cli installed
# 2. Make sure "server = 1" is in bitcoin.conf
# 3. Run this file as "python3 UTXOracle.py"
# If this isn't working for you, you'll likely need to explore the
# bitcon-cli configuration options below:
# configuration options for bitcoin-cli
datadir = ""
rpcuser = ""
rpcpassword = ""
rpcookiefile = ""
rpcconnect = ""
rpcport = ""
conf = ""
#add the configuration options to the bitcoin-cli call
bitcoin_cli_options = []
if datadir != "":
bitcoin_cli_options.append('-datadir='+ datadir)
if rpcuser != "":
bitcoin_cli_options.append("-rpcuser="+ rpcuser)
if rpcpassword != "":
bitcoin_cli_options.append("-rpcpassword="+ rpcpassword)
if rpcookiefile != "":
bitcoin_cli_options.append("-rpcookiefile="+ rpcookiefile)
if rpcconnect != "":
bitcoin_cli_options.append("-rpcconnect="+ rpcconnect)
if rpcport != "":
bitcoin_cli_options.append("-rpcport="+ rpcport)
if conf != "":
bitcoin_cli_options.append("-conf="+ conf)
###############################################################################
# Part 1) Defining a shortcut function to call your node
###############################################################################
# Here we define define a shortcut (a function) for calling the node as we do
# this many times through out the program. The function will return the
# answer it gets from your node with the "command" that you asked it for.
# If we don't get an answer, the problem is likely that you don't have
# sever=1 in your bitcoin conf file.
import subprocess #a built in python library for sending command line commands
def Ask_Node(command):
# 'bitcoin-cli' is how the command window calls your node so
# it needs to be the first word in any request for data from the node
# other options are added if given
for o in bitcoin_cli_options:
command.insert(0,o)
command.insert(0,"bitcoin-cli")
# get the answer from the node and return it to the program
answer = None
try: #python try is used when we need to deal with errors after
answer = subprocess.check_output(command)
except Exception as e:
# something went wrong while getting the answer
print("Error connecting to your node. Trouble shooting steps:\n")
print("\t 1) Make sure bitcoin-cli is working. Try command 'bitcoin-cli getblockcount'")
print("\t 2) Make sure config file bitcoin.conf has server=1")
print("\t 3) Explore the bitcoin-cli options in UTXOracle.py (line 38)")
print("\nThe command was:"+str(command))
print("\nThe error from bitcoin-cli was:\n")
print(e)
exit()
# answer received, return this answer to the program
return answer
###############################################################################
# Part 2) Get the latest block from the node
###############################################################################
# The first request to the node is to ask it how many blocks it has. This
# let's us know the maximum possible day for which we can request a
# btc price estimate. The time information of blocks is listed in the block
# header, so we ask for the header only when we just need to know the time.
#get current block height from local node and exit if connection not made
block_count_b = Ask_Node(['getblockcount'])
block_count = int(block_count_b) #convert text to integer
#get block header from current block height
block_hash_b = Ask_Node(['getblockhash',block_count_b])
block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],'true'])
import json #a built in tool for deciphering lists of embedded lists
block_header = json.loads(block_header_b)
#get the date and time of the current block height
from datetime import datetime, timezone #built in tools for dates/times
latest_time_in_seconds = block_header['time']
time_datetime = datetime.fromtimestamp(latest_time_in_seconds,tz=timezone.utc)
#get the date/time of utc midnight on the latest day
latest_year = int(time_datetime.strftime("%Y"))
latest_month = int(time_datetime.strftime("%m"))
latest_day = int(time_datetime.strftime("%d"))
latest_utc_midnight = datetime(latest_year,latest_month,latest_day,0,0,0,tzinfo=timezone.utc)
#assign the day before as the latest possible price date
seconds_in_a_day = 60*60*24
yesterday_seconds = latest_time_in_seconds - seconds_in_a_day
latest_price_day = datetime.fromtimestamp(yesterday_seconds,tz=timezone.utc)
latest_price_date = latest_price_day.strftime("%Y-%m-%d")
# tell the user that a connection has been made and state the lastest price date
print("Connected to local noode at block #:\t"+str(block_count))
print("Latest available price date is: \t"+latest_price_date)
print("Earliest available price date is:\t2020-07-26 (full node)")
###############################################################################
# Part 3) Ask for the desired date to estimate the price
###############################################################################
#use python input to get date from the user
date_entered = input("\nEnter date in YYYY-MM-DD (or 'q' to quit):")
# quit if desired
if date_entered == 'q':
exit()
#check to see if this is a good date
try:
year = int(date_entered.split('-')[0])
month = int(date_entered.split('-')[1])
day = int(date_entered.split('-')[2])
#make sure this date is less than the max date
datetime_entered = datetime(year,month,day,0,0,0,tzinfo=timezone.utc)
if datetime_entered.timestamp() >= latest_utc_midnight.timestamp():
print("\nThe date entered is not before the current date, please try again")
exit()
#make sure this date is after the min date
july_26_2020 = datetime(2020,7,26,0,0,0,tzinfo=timezone.utc)
if datetime_entered.timestamp() < july_26_2020.timestamp():
print("\nThe date entered is before 2020-07-26, please try again")
exit()
except:
print("\nError interpreting date. Likely not entered in format YYYY-MM-DD")
print("Please try again\n")
exit()
#get the seconds and printable date string of date entered
price_day_seconds = int(datetime_entered.timestamp())
price_day_date_utc = datetime_entered.strftime("%B %d, %Y")
##############################################################################
# Part 4) Hunt through blocks to find the first block on the target day
##############################################################################
# This section would be unnecessary if bitcoin Core blocks were organized by time
# instead of by block height. There's no way to ask bitcoin Core for a block at a
# specific time. Instead one must ask for a block, look at it's time, then estimate
# the number of blocks to jump for the next guess. Rinse and repeat.
#first estimate of the block height of the price day
seconds_since_price_day = latest_time_in_seconds - price_day_seconds
blocks_ago_estimate = round(144*float(seconds_since_price_day)/float(seconds_in_a_day))
price_day_block_estimate = block_count - blocks_ago_estimate
#check the time of the price day block estimate
block_hash_b = Ask_Node(['getblockhash',str(price_day_block_estimate)])
block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],'true'])
block_header = json.loads(block_header_b)
time_in_seconds = block_header['time']
#get new block estimate from the seconds difference using 144 blocks per day
seconds_difference = time_in_seconds - price_day_seconds
block_jump_estimate = round(144*float(seconds_difference)/float(seconds_in_a_day))
#iterate above process until it oscillates around the correct block
last_estimate = 0
last_last_estimate = 0
while block_jump_estimate >6 and block_jump_estimate != last_last_estimate:
#when we osciallate around the correct block, last_last_estimate = block_jump_estimate
last_last_estimate = last_estimate
last_estimate = block_jump_estimate
#get block header or new estimate
price_day_block_estimate = price_day_block_estimate-block_jump_estimate
block_hash_b = Ask_Node(['getblockhash',str(price_day_block_estimate)])
block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],'true'])
block_header = json.loads(block_header_b)
#check time of new block and get new block jump estimate
time_in_seconds = block_header['time']
seconds_difference = time_in_seconds - price_day_seconds
block_jump_estimate = round(144*float(seconds_difference)/float(seconds_in_a_day))
#the oscillation may be over multiple blocks so we add/subtract single blocks
#to ensure we have exactly the first block of the target day
if time_in_seconds > price_day_seconds:
# if the estimate was after price day look at earlier blocks
while time_in_seconds > price_day_seconds:
#decrement the block by one, read new block header, check time
price_day_block_estimate = price_day_block_estimate-1
block_hash_b = Ask_Node(['getblockhash',str(price_day_block_estimate)])
block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],'true'])
block_header = json.loads(block_header_b)
time_in_seconds = block_header['time']
#the guess is now perfectly the first block before midnight
price_day_block_estimate = price_day_block_estimate + 1
# if the estimate was before price day look for later blocks
elif time_in_seconds < price_day_seconds:
while time_in_seconds < price_day_seconds:
#increment the block by one, read new block header, check time
price_day_block_estimate = price_day_block_estimate+1
block_hash_b = Ask_Node(['getblockhash',str(price_day_block_estimate)])
block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],'true'])
block_header = json.loads(block_header_b)
time_in_seconds = block_header['time']
#assign the estimate as the price day block since it is correct now
price_day_block = price_day_block_estimate
##############################################################################
# Part 5) Build the container to hold the output amounts bell curve
##############################################################################
# In pure math a bell curve can be perfectly smooth. But to make a bell curve
# from a sample of data, one must specifiy a series of buckets, or bins, and then
# count how many samples are in each bin. If the bin size is too large, say just one
# large bin, a bell curve can't appear because it will have only one bar. The bell
# curve also doesn't appear if the bin size is too small because then there will
# only be one sample in each bin and we'd fail to have a distribution of bin heights.
# Although several bin sizes would work, I have found over many years, that 200 bins
# for every 10x of bitcoin amounts works very well. We use 'every 10x' because just
# like a long term bitcoin price chart, viewing output amounts in log scale provides
# a more comprehensive and detailed overview of the amounts being analyzed.
# Define the maximum and minimum values (in log10) of btc amounts to use
first_bin_value = -6
last_bin_value = 6 #python -1 means last in list
range_bin_values = last_bin_value - first_bin_value
# create a list of output_bell_curve_bins and add zero sats as the first bin
output_bell_curve_bins = [0.0] #a decimal tells python the list will contain decimals
# calculate btc amounts of 200 samples in every 10x from 100 sats (1e-6 btc) to 100k (1e5) btc
for exponent in range(-6,6): #python range uses 'less than' for the big number
#add 200 bin_width increments in this 10x to the list
for b in range(0,200):
bin_value = 10 ** (exponent + b/200)
output_bell_curve_bins.append(bin_value)
# Create a list the same size as the bell curve to keep the count of the bins
number_of_bins = len(output_bell_curve_bins)
output_bell_curve_bin_counts = []
for n in range(0,number_of_bins):
output_bell_curve_bin_counts.append(float(0.0))
##############################################################################
# Part 6) Get all output amounts from all block on target day
##############################################################################
# This section of the program will take the most time as it requests all
# all blocks from Core on the price day. It readers every transaction (tx)
# from those blocks and places each tx output value into the bell curve
from math import log10 #built in math functions needed logarithms
#print header line of update table
print("\nReading all blocks on "+price_day_date_utc+"...")
print("\nThis will take a few minutes (~144 blocks)...")
print("\nHeight\tTime(utc)\t\tTime(32bit)\t\t Completion %")
TXIDS = set()
#get the full data of the first target day block from the node
block_height=price_day_block
block_hash_b = Ask_Node(['getblockhash',str(block_height)])
block_b = Ask_Node(['getblock',block_hash_b[:64],'2'])
block = json.loads(block_b)
#get the time of the first block
time_in_seconds = int(block['time'])
time_datetime = datetime.fromtimestamp(time_in_seconds,tz=timezone.utc)
time_utc = time_datetime.strftime("%H:%M:%S")
hour_of_day = int(time_datetime.strftime("%H"))
minute_of_hour = float(time_datetime.strftime("%M"))
day_of_month = int(time_datetime.strftime("%d"))
target_day_of_month = day_of_month
time_32bit = f"{time_in_seconds & 0b11111111111111111111111111111111:32b}"
#read in blocks until we get a block on the day after the target day
while target_day_of_month == day_of_month:
#get progress estimate
progress_estimate = 100.0*(hour_of_day+minute_of_hour/60)/24.0
#print progress update
print(str(block_height)+"\t"+time_utc+"\t"+time_32bit+"\t"+f"{progress_estimate:.2f}"+"%")
#go through all the txs in the block which are stored in a list called 'tx'
for tx in block['tx']:
TXIDS.add(tx['txid'])
is_economic = True
for vin in tx['vin']:
if 'txid' in vin:
if vin['txid'] in TXIDS:
is_economic = False
#txs have more than one output which are stored in a list called 'vout'
if is_economic:
outputs = tx['vout']
#go through all outputs in the tx
for output in outputs:
#the bitcoin output amount is called 'value' in Core, add this to the list
amount = float(output['value'])
#tiny and huge amounts aren't used by the USD price finder
if 1e-6 < amount < 1e6:
#take the log
amount_log = log10(amount)
#find the right output amount bin to increment
percent_in_range = (amount_log-first_bin_value)/range_bin_values
bin_number_est = int(percent_in_range * number_of_bins)
#search for the exact right bin (won't be less than)
while output_bell_curve_bins[bin_number_est] <= amount:
bin_number_est += 1
bin_number = bin_number_est - 1
#increment the output bin
output_bell_curve_bin_counts[bin_number] += 1.0 #+= means increment
#get the full data of the next block
block_height = block_height + 1
block_hash_b = Ask_Node(['getblockhash',str(block_height)])
block_b = Ask_Node(['getblock',block_hash_b[:64],'2'])
block = json.loads(block_b)
#get the time of the next block
time_in_seconds = int(block['time'])
time_datetime = datetime.fromtimestamp(time_in_seconds,tz=timezone.utc)
time_utc = time_datetime.strftime("%H:%M:%S")
day_of_month = int(time_datetime.strftime("%d"))
minute_of_hour = float(time_datetime.strftime("%M"))
hour_of_day = int(time_datetime.strftime("%H"))
time_32bit = f"{time_in_seconds & 0b11111111111111111111111111111111:32b}"
##############################################################################
# Part 7) Remove non-usd related output amounts from the bell curve
##############################################################################
# This sectoins aims to remove non-usd denominated samples from the bell curve
# of outputs. The two primary steps are to remove very large/small outputs
# and then to remove round btc amounts. We don't set the round btc amounts
# to zero because if the USD price of bitcoin is also round, then round
# btc amounts will co-align with round usd amounts. There are many ways to deal
# with this. One way we've found to work is to smooth over the round btc amounts
# using the neighboring amounts in the bell curve. The last step is to normalize
# the bell curve. Normalizing is done by dividing the entire curve by the sum
# of the curve, and then removing extreme values.
#remove ouputs below 10k sat (increased from 1k sat in v6)
for n in range(0,401):
output_bell_curve_bin_counts[n]=0
#remove outputs above ten btc
for n in range(1601,number_of_bins):
output_bell_curve_bin_counts[n]=0
#create a list of round btc bin numbers
round_btc_bins = [
201, # 1k sats
401, # 10k
461, # 20k
496, # 30k
540, # 50k
601, # 100k
661, # 200k
696, # 300k
740, # 500k
801, # 0.01 btc
861, # 0.02
896, # 0.03
940, # 0.04
1001, # 0.1
1061, # 0.2
1096, # 0.3
1140, # 0.5
1201 # 1 btc
]
#smooth over the round btc amounts
for r in round_btc_bins:
amount_above = output_bell_curve_bin_counts[r+1]
amount_below = output_bell_curve_bin_counts[r-1]
output_bell_curve_bin_counts[r] = .5*(amount_above+amount_below)
#get the sum of the curve
curve_sum = 0.0
for n in range(201,1601):
curve_sum += output_bell_curve_bin_counts[n]
#normalize the curve by dividing by it's sum and removing extreme values
for n in range(201,1601):
output_bell_curve_bin_counts[n] /= curve_sum
#remove extremes (the iterative process mentioned below found 0.008 to work)
if output_bell_curve_bin_counts[n] > 0.008:
output_bell_curve_bin_counts[n] = 0.008
##############################################################################
# Part 8) Construct the USD price finder stencil
##############################################################################
# We now have a bell curve of outputs which should contain round USD outputs
# as it's prominent features. To expose these prominent features even more,
# and estimate a usd price, we slide a stencil over the bell curve and look
# for where the slide location is maximized. There are several stencil designs
# and maximization strategies which could accomplish this. The one used here
# is a stencil whose locations and heights have been found using the averages of
# running this algorithm iteratively over the years 2020-2023. The stencil is
# centered around 0.01 btc = $10,00 as this was the easiest mark to identify.
# The result of this process has produced the following stencil design:
# create an empty stencil the same size as the bell curve
round_usd_stencil = []
for n in range(0,number_of_bins):
round_usd_stencil.append(0.0)
# fill the round usd stencil with the values found by the process mentioned above
round_usd_stencil[401] = 0.0005957955691168063 # $1
round_usd_stencil[402] = 0.0004454790662303128 # (next one for tx/atm fees)
round_usd_stencil[429] = 0.0001763099393598914 # $1.50
round_usd_stencil[430] = 0.0001851801497144573
round_usd_stencil[461] = 0.0006205616481885794 # $2
round_usd_stencil[462] = 0.0005985696860584984
round_usd_stencil[496] = 0.0006919505728046619 # $3
round_usd_stencil[497] = 0.0008912933078342840
round_usd_stencil[540] = 0.0009372916238804205 # $5
round_usd_stencil[541] = 0.0017125522985034724 # (larger needed range for fees)
round_usd_stencil[600] = 0.0021702347223143030
round_usd_stencil[601] = 0.0037018622326411380 # $10
round_usd_stencil[602] = 0.0027322168706743802
round_usd_stencil[603] = 0.0016268322583097678 # (larger needed range for fees)
round_usd_stencil[604] = 0.0012601953416497664
round_usd_stencil[661] = 0.0041425242880295460 # $20
round_usd_stencil[662] = 0.0039247767475640830
round_usd_stencil[696] = 0.0032399441632017228 # $30
round_usd_stencil[697] = 0.0037112959007355585
round_usd_stencil[740] = 0.0049921908828370000 # $50
round_usd_stencil[741] = 0.0070636869018197105
round_usd_stencil[801] = 0.0080000000000000000 # $100
round_usd_stencil[802] = 0.0065431388282424440 # (larger needed range for fees)
round_usd_stencil[803] = 0.0044279509203361735
round_usd_stencil[861] = 0.0046132440551747015 # $200
round_usd_stencil[862] = 0.0043647851395531140
round_usd_stencil[896] = 0.0031980892880846567 # $300
round_usd_stencil[897] = 0.0034237641632481910
round_usd_stencil[939] = 0.0025995335505435034 # $500
round_usd_stencil[940] = 0.0032631930982226645 # (larger needed range for fees)
round_usd_stencil[941] = 0.0042753262790881080
round_usd_stencil[1001] =0.0037699501474772350 # $1,000
round_usd_stencil[1002] =0.0030872891064215764 # (larger needed range for fees)
round_usd_stencil[1003] =0.0023237040836798163
round_usd_stencil[1061] =0.0023671764210889895 # $2,000
round_usd_stencil[1062] =0.0020106877104798474
round_usd_stencil[1140] =0.0009099214128654502 # $3,000
round_usd_stencil[1141] =0.0012008546799361498
round_usd_stencil[1201] =0.0007862586076341524 # $10,000
round_usd_stencil[1202] =0.0006900048077192579
##############################################################################
# Part 9) Slide the stencil over the output bell curve to find the best fit
##############################################################################
# This is the final step. We slide the stencil over the bell curve and see
# where it fits the best. The best fit location and it's neighbor are used
# in a weighted average to estimate the best fit USD price
# set up scores for sliding the stencil
best_slide = 0
best_slide_score = 0.0
total_score = 0.0
number_of_scores = 0
#upper and lower limits for sliding the stencil
min_slide = -200
max_slide = 200
#slide the stencil and calculate slide score
for slide in range(min_slide,max_slide):
#shift the bell curve by the slide
shifted_curve = output_bell_curve_bin_counts[201+slide:1401+slide]
#score the shift by multiplying the curve by the stencil
slide_score = 0.0
for n in range(0,len(shifted_curve)):
slide_score += shifted_curve[n]*round_usd_stencil[n+201]
# increment total and number of scores
total_score += slide_score
number_of_scores += 1
# see if this score is the best so far
if slide_score > best_slide_score:
best_slide_score = slide_score
best_slide = slide
# estimate the usd price of the best slide
usd100_in_btc_best = output_bell_curve_bins[801+best_slide]
btc_in_usd_best = 100/(usd100_in_btc_best)
#find best slide neighbor up
neighbor_up = output_bell_curve_bin_counts[201+best_slide+1:1401+best_slide+1]
neighbor_up_score = 0.0
for n in range(0,len(neighbor_up)):
neighbor_up_score += neighbor_up[n]*round_usd_stencil[n+201]
#find best slide neighbor down
neighbor_down = output_bell_curve_bin_counts[201+best_slide-1:1401+best_slide-1]
neighbor_down_score = 0.0
for n in range(0,len(neighbor_down)):
neighbor_down_score += neighbor_down[n]*round_usd_stencil[n+201]
#get best neighbor
best_neighbor = +1
neighbor_score = neighbor_up_score
if neighbor_down_score > neighbor_up_score:
best_neighbor = -1
neighbor_score = neighbor_down_score
#get best neighbor usd price
usd100_in_btc_2nd = output_bell_curve_bins[801+best_slide+best_neighbor]
btc_in_usd_2nd = 100/(usd100_in_btc_2nd)
#weight average the two usd price estimates
avg_score = total_score/number_of_scores
a1 = best_slide_score - avg_score
a2 = abs(neighbor_score - avg_score) #theoretically possible to be negative
w1 = a1/(a1+a2)
w2 = a2/(a1+a2)
price_estimate = int(w1*btc_in_usd_best + w2*btc_in_usd_2nd)
#report the price estimate
print("\nThe "+price_day_date_utc+" btc price estimate is: $" + f'{price_estimate:,}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment