forked from Unbesteveable/UTXOracle
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUTXOracle.py
More file actions
1800 lines (1383 loc) · 61.8 KB
/
UTXOracle.py
File metadata and controls
1800 lines (1383 loc) · 61.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#########################################################################################
# #
# /$$ /$$ /$$$$$$$$ /$$ /$$ /$$$$$$ /$$ #
# | $$ | $$|__ $$__/| $$ / $$ /$$__ $$ | $$ #
# | $$ | $$ | $$ | $$/ $$/| $$ \ $$ /$$$$$$ /$$$$$$ /$$$$$$$| $$ /$$$$$$ #
# | $$ | $$ | $$ \ $$$$/ | $$ | $$ /$$__ $$|____ $$ /$$_____/| $$ /$$__ $$ #
# | $$ | $$ | $$ >$$ $$ | $$ | $$| $$ \__/ /$$$$$$$| $$ | $$| $$$$$$$$ #
# | $$ | $$ | $$ /$$/\ $$| $$ | $$| $$ /$$__ $$| $$ | $$| $$_____/ #
# | $$$$$$/ | $$ | $$ \ $$| $$$$$$/| $$ | $$$$$$$| $$$$$$$| $$| $$$$$$$ #
# \______/ |__/ |__/ |__/ \______/ |__/ \_______/ \_______/|__/ \_______/ #
# #
#########################################################################################
# Version 9.1 RPC Only
###############################################################################
# Introduction
###############################################################################
# Thank you for taking the time to open this. Computer programs have two functions:
# one for the computer and one for the human. This might be the first time you've
# attempted to read a computer program. If so, these lines starting with the hash tag
# are for you. They’ll tell you what the code is doing at all times. The non–hash-tag
# lines are used by the computer, though if you spend a few seconds with them,
# you’ll be able to understand exactly what they’re doing.
# Since you opened this, you probably already have a good idea of what UTXOracle
# does. It finds the price of Bitcoin using data only from your own node. It doesn’t
# contact any third parties. The code also has no third-party dependencies. It runs
# on the most basic version of Python 3 that you can find. Many operating systems
# have Python 3 built in, but many don’t. You’ll also need to know how to open a
# terminal window on your computer. To see if you already have Python 3 installed,
# open a terminal window and type python3 --version, then hit Enter. If it doesn’t
# show you a version number, you’ll need to download and install it from python.org.
# If you’re reading this from a file on your computer, then you already have the
# program. If you’re reading this off a website, you need to download it to your
# computer. You can do this with the simple command "curl -O https://utxo.live/oracle/UTXOracle.py".
# It will download this single file as UTXOracle.py. Then run the program with
# "python3 UTXOracle.py" which will find yesterday's price by default. To run the program
# on different days, and up to the latest block, type "python3 UTXOracle.py -h"
# to see the command options.
# If you’re an experienced coder, you’ll notice many inefficiencies in this code.
# It is a single file that runs top to bottom.
# It doesn’t not make use of advanced libraries that would make the code more
# efficient at the cost of third party dependence. It repeats code for clarity instead of
# defining functions where the user has to constantly scroll up and down and to
# other files to see how the function works. The purpose of this code is not for
# efficiency, or for corporate team implementations. The purpose is to explain how
# the UTXOracle algorithm works side by side with the code.
# The code proceeds by completing the following 12 steps in order. Approximate line
# numbers of the steps are listed for the user to jump directly there if desired.
# Step 1 - Configuration Options...................Line 78
# Step 2 - Establish RPC Connection................Line 206
# Step 3 - Check Dates.............................Line 315
# Step 4 - Find Block Hashes.......................Line 409
# Step 5 - Initialize Histogram....................Line 589
# Step 6 - Load Histogram from Transaction Data....Line 646
# Step 7 - Remove Round Bitcoin Amounts............Line 889
# Step 8 - Construct the Price Finding Stencil.....Line 971
# Step 9 - Estimate a Rough Price..................Line 1049
# Step 10 - Create Intraday Price Points...........Line 1160
# Step 11 - Find the Exact Average Price...........Line 1260
# Step 12 - Generate a Price Plot HTML Page........Line 1377
###############################################################################
# Step 1 - Configuation Options
###############################################################################
# The way UTXOracle connects to your Bitcoin node depends on your operating
# system and the settings in your bitcoin.conf file. First, the program determines
# your operating system to make an initial guess at the default location of your
# primary Bitcoin directory. If you're not using the default directory, use the -p option
# when running the program to specify the correct block directory.
# Other options let you specify the historical date you want to run (-h) or request the
# price from the most recent 144 blocks (-rb). If you're an advanced user with
# custom settings overriding the default RPC connection, the program will read your
# bitcoin.conf file to use them. Otherwise, it will use the autogenerated cookie file for
# authentication.
# set platform dependent data paths
import platform
import os
data_dir = []
system = platform.system()
if system == "Darwin": # macOS
data_dir = os.path.expanduser("~/Library/Application Support/Bitcoin")
elif system == "Windows":
data_dir = os.path.join(os.environ.get("APPDATA", ""), "Bitcoin")
else: # Linux or others
data_dir = os.path.expanduser("~/.bitcoin")
# initialize variables for blocks and dates
date_entered = ""
date_mode = True
block_mode = False
block_start_num = 0
block_finish_num = 0
block_nums_needed = []
block_hashes_needed = []
block_times_needed = []
#print help text for the user if needed
import sys
def print_help():
help_text = """
Usage: python3 UTXOracle.py [options]
Options:
-h Show this help message
-d YYYY/MM/DD Specify a UTC date to evaluate
-p /path/to/dir Specify the data directory for blk files
-rb Use last 144 recent blocks instead of date mode
"""
print(help_text)
sys.exit(0)
#did use ask for help
if "-h" in sys.argv:
print_help()
#did user specify a date?
if "-d" in sys.argv:
h_index = sys.argv.index("-d")
if h_index + 1 < len(sys.argv):
date_entered = sys.argv[h_index + 1]
#did user specify a data path?
if "-p" in sys.argv:
d_index = sys.argv.index("-p")
if d_index + 1 < len(sys.argv):
data_dir = sys.argv[d_index + 1]
#did user specify blocks instead of a date?
if "-rb" in sys.argv:
date_mode = False
block_mode = True
# Look for bitcoin.conf or bitcoin_rw.conf
conf_path = None
conf_candidates = ["bitcoin.conf", "bitcoin_rw.conf"]
for fname in conf_candidates:
path = os.path.join(data_dir, fname)
if os.path.exists(path):
conf_path = path
break
if not conf_path:
print(f"Invalid Bitcoin data directory: {data_dir}")
print("Expected to find 'bitcoin.conf' or 'bitcoin_rw.conf' in this directory.")
sys.exit(1)
# Parse the conf file
conf_settings = {}
with open(conf_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, value = line.split("=", 1)
conf_settings[key.strip()] = value.strip().strip('"')
# Set blocks directory
blocks_dir = os.path.expanduser(conf_settings.get("blocksdir", os.path.join(data_dir, "blocks")))
# Prepare RPC parameters
rpc_user = conf_settings.get("rpcuser")
rpc_password = conf_settings.get("rpcpassword")
cookie_path = conf_settings.get("rpccookiefile", os.path.join(data_dir, ".cookie"))
rpc_host = conf_settings.get("rpcconnect", "127.0.0.1")
rpc_port = int(conf_settings.get("rpcport", "8332"))
# Prepare bitcoin-cli fallback options (if needed elsewhere)
bitcoin_cli_options = []
if rpc_user and rpc_password:
bitcoin_cli_options.append(f"-rpcuser={rpc_user}")
bitcoin_cli_options.append(f"-rpcpassword={rpc_password}")
else:
if os.path.exists(cookie_path):
bitcoin_cli_options.append(f"-rpccookiefile={cookie_path}")
if rpc_host:
bitcoin_cli_options.append(f"-rpcconnect={rpc_host}")
if "rpcport" in conf_settings:
bitcoin_cli_options.append(f"-rpcport={conf_settings['rpcport']}")
###############################################################################
# Step 2 - Establish RPC Connection
###############################################################################
# Community consensus has established that RPC should be the standard front door
# through which other programs communicate with your Bitcoin node. Since we'll be
# making repeated requests to the node, we define a general function that attaches
# the necessary RPC credentials to any command we send and returns the
# response to where it's needed in the program. If this is the first time you've used
# RPC, the function will create the RPC cookie file using the node's default method.
# Otherwise, it simply returns the result of the command and prints any errors
# encountered. After defining the Ask_Node function, we test it by requesting the
# latest block the node has received.
print("\nCurrent operation \t\t\t\tTotal Completion",flush=True)
print("\nConnecting to node...",flush=True)
print("0%..", end="",flush=True)
# define the node communication function
import http.client
import json
import base64
def Ask_Node(command, cred_creation):
method = command[0]
params = command[1:]
# Handle authentication
rpc_u = rpc_user
rpc_p = rpc_password
if not rpc_u or not rpc_p:
try:
with open(cookie_path, "r") as f:
cookie = f.read().strip()
rpc_u, rpc_p = cookie.split(":", 1)
except Exception as e:
print("Error reading .cookie file for RPC authentication.")
print("Details:", e)
sys.exit(1)
# Prepare JSON-RPC payload
payload = json.dumps({
"jsonrpc": "1.0",
"id": "utxoracle",
"method": method,
"params": params
})
# Basic auth header
auth_header = base64.b64encode(f"{rpc_u}:{rpc_p}".encode()).decode()
headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {auth_header}"
}
try:
conn = http.client.HTTPConnection(rpc_host, rpc_port)
conn.request("POST", "/", payload, headers)
response = conn.getresponse()
if response.status != 200:
raise Exception(f"HTTP error {response.status} {response.reason}")
raw_data = response.read()
conn.close()
# Extract result and mimic `subprocess.check_output` return (as bytes)
parsed = json.loads(raw_data)
if parsed.get("error"):
raise Exception(parsed["error"])
result = parsed["result"]
if isinstance(result, (dict, list)):
return json.dumps(result, indent=2).encode() # pretty-print like CLI
else:
return str(result).encode()
except Exception as e:
if not cred_creation:
print("Error connecting to your node via RPC. Troubleshooting steps:\n")
print("\t1) Ensure bitcoind or bitcoin-qt is running with server=1.")
print("\t2) Check rpcuser/rpcpassword or .cookie.")
print("\t3) Verify RPC port/host settings.")
print("\nThe attempted RPC method was:", method)
print("Parameters:", params)
print("\nThe error was:\n", e)
sys.exit(1)
#get current block height from local node and exit if connection not made
print("20%..", end="",flush=True)
Ask_Node(['getblockcount'], True) #create RPC creds if necessary
block_count_b = Ask_Node(['getblockcount'], False)
print("40%..", end="",flush=True)
block_count = int(block_count_b) #convert text to integer
block_count_consensus = block_count-6
#get block header from current block height
#block_hash = Ask_Node(['getblockhash', str(block_count_consensus)]).decode().strip()
block_hash = Ask_Node(['getblockhash',block_count_consensus], False).decode().strip()
print("60%..", end="",flush=True)
block_header_b = Ask_Node(['getblockheader', block_hash, True], False)
block_header = json.loads(block_header_b)
print("80%..", end="",flush=True)
###############################################################################
# Step 3 - Check Dates
###############################################################################
# The Bitcoin price does not exist instantaneously on-chain. A single block often
# does not contain enough transaction data to determine the price—and in some
# cases, may contain no transactions at all. For this reason, a time-averaging
# window is required to accumulate sufficient data to estimate the price accurately.
# While many averaging windows could work, a single day is both a natural human
# time scale and typically includes enough transaction activity. For accounting and
# historical purposes, daily resolution is also a commonly used standard. Therefore,
# UTXOracle uses the daily average as its default time window for determining price.
# This means UTXOracle must know two things: (1) the most recent date covered by
# the node’s block data, and (2) the date the user is requesting the price for. If the
# requested date is too far in the past or future relative to the available data, an error
# message will be displayed. The earliest supported date depends on how far back
# the current version of UTXOracle has been tested.
#import built in tools for dates/times and json style lists
from datetime import datetime, timezone, timedelta
#get the date and time of the current block height
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")
print("100%", end="",flush=True)
#print completion update
print("\t\t\t5% done",flush=True)
# If running in date mode, make sure that the date requested is in the
if date_mode:
# run latest day if hit enter
if (date_entered == ""):
datetime_entered = latest_utc_midnight + timedelta(days=-1)
#user entered a specific date
else:
#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("\nDate is after the latest avaiable. We need 6 blocks after UTC midnight.")
print("Run UTXOracle.py -rb for the most recent blocks")
sys.exit()
#make sure this date is after the min date
dec_15_2023 = datetime(2023,12,15,0,0,0,tzinfo=timezone.utc)
if datetime_entered.timestamp() < dec_15_2023.timestamp():
print("\nThe date entered is before 2023-12-15, please try again")
sys.exit()
except:
print("\nError interpreting date. Please try again. Make sure format is YYYY/MM/DD")
sys.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")
price_date_dash = datetime_entered.strftime("%Y-%m-%d")
##############################################################################
# Step 4 - Find Block Hashes
##############################################################################
# Bitcoin nodes don’t store blocks by date—or even by block height. Instead, they
# store blocks by their hash. This has two implications: first, we need to know which
# block heights we're looking for, and second, we must retrieve the corresponding
# block hashes for those heights.
# The task is simpler when running in block mode (-rb), where we determine the
# needed blocks by subtracting 144 from the most recent block height. In date mode,
# however, the process is more involved, as we must enter a guess-and-check loop
# to determine which blocks correspond to the desired calendar day.
# Since blocks are mined roughly every ten minutes, we can use that as a guideline.
# When a user enters a date, we first estimate how many blocks ago that date
# occurred. We then check block timestamps starting from our estimate, moving
# forward to find the last block of that day, and backward to find the first block.
# Once we’ve identified all the relevant block heights, we store the hashes of each
# block in a list so that we can retrieve them one by one in the next step.
#define a shortcut for getting the block time from the block number
def get_block_time(height):
block_hash_b = Ask_Node(['getblockhash',height], False)
#block_header_b = Ask_Node(['getblockheader',block_hash_b[:64],True])
block_header_b = Ask_Node(['getblockheader', block_hash_b.decode().strip(), True], False)
block_header = json.loads(block_header_b)
return(block_header['time'], block_hash_b[:64].decode())
#define a shortcut for getting the day of money from a time in seconds
def get_day_of_month(time_in_seconds):
time_datetime = datetime.fromtimestamp(time_in_seconds,tz=timezone.utc)
return(int(time_datetime.strftime("%d")))
#if block mode add the blocks and hashes to a list
if block_mode:
print("\nFinding the last 144 blocks",flush=True)
#get the last block number of the day
block_finish_num = block_count
block_start_num = block_finish_num - 144
#append needed block nums and hashes needed
block_num = block_start_num
time_in_seconds, hash_end = get_block_time(block_start_num)
print_every = 0
while block_num < block_finish_num:
#print update
if (block_num-block_start_num)/144*100 > print_every and print_every < 100:
print(str(print_every)+"%..",end="",flush=True)
print_every += 20
block_nums_needed.append(block_num)
block_hashes_needed.append(hash_end)
block_times_needed.append(time_in_seconds)
block_num += 1
time_in_seconds, hash_end = get_block_time(block_num)
print("100%\t\t\t25% done",flush=True)
#if date mode search for all the blocks on this day
elif date_mode:
print("\nFinding first blocks on "+datetime_entered.strftime("%b %d, %Y"),flush=True)
print("0%..",end="", flush=True)
#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_consensus - blocks_ago_estimate
#check the time of the price day block estimate
time_in_seconds, hash_end = get_block_time(price_day_block_estimate)
#get new block estimate from the seconds difference using 144 blocks per day
print("20%..",end="",flush=True)
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
print("40%..",end="",flush=True)
while block_jump_estimate >6 and block_jump_estimate != last_last_estimate:
#when we oscillate 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
#check time of new block and get new block jump estimate
time_in_seconds, hash_end = get_block_time(price_day_block_estimate)
seconds_difference = time_in_seconds - price_day_seconds
block_jump_estimate = round(144*float(seconds_difference)/float(seconds_in_a_day))
print("60%..",end="",flush=True)
#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
time_in_seconds, hash_end = get_block_time(price_day_block_estimate)
#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
time_in_seconds, hash_end = get_block_time(price_day_block_estimate)
print("80%..",end="",flush=True)
#assign the estimate as the price day block since it is correct now
price_day_block = price_day_block_estimate
#get the day of the month
time_in_seconds, hash_start = get_block_time(price_day_block)
day1 = get_day_of_month(time_in_seconds)
#get the last block number of the day
price_day_block_end = price_day_block
time_in_seconds, hash_end = get_block_time(price_day_block_end)
day2 = get_day_of_month(time_in_seconds)
print("100%\t\t\t25% done",flush=True)
print("\nFinding last blocks on "+datetime_entered.strftime("%b %d, %Y"),flush=True)
#load block nums and hashes needed
block_num = 0
print_next = 0
while day1 == day2:
#print progress update
block_num+=1
if block_num/144 * 100 > print_next:
if print_next < 100:
print(str(print_next)+"%..",end="",flush=True)
print_next +=20
#append needed block
block_nums_needed.append(price_day_block_end)
block_hashes_needed.append(hash_end)
block_times_needed.append(time_in_seconds)
price_day_block_end += 1 #assume 30+ blocks this day
time_in_seconds, hash_end = get_block_time(price_day_block_end)
day2 = get_day_of_month(time_in_seconds)
#complete print update status
while print_next<100:
print(str(print_next)+"%..",end="",flush=True)
print_next +=20
#set start and end block numbers
block_start_num = price_day_block
block_finish_num = price_day_block_end
print("100%\t\t\t50% done",flush=True)
##############################################################################
# Step 5 - Initial histogram
##############################################################################
# Just as the price of Bitcoin does not exist at a single instant in time, it also does not
# exist at a single satoshi amount. The on-chain price emerges because users tend
# to spend Bitcoin in round fiat amounts. To detect this, we create a histogram that
# reveals clusters of satoshi values where round fiat amounts are most likely to
# occur.
# We divide the BTC value range into intervals and count how many transaction
# outputs fall into each interval. This allows us to visualize where spending patterns
# cluster. If the intervals are too small, the data becomes noisy; if they’re too large,
# important detail is lost. A rough estimate of the ideal interval width is the average
# daily fiat volatility, which we’ve found to be around 0.5%—corresponding to roughly
# 200 intervals between each power of ten in BTC.
# We must also define the upper and lower bounds of the histogram: the smallest
# and largest BTC amounts likely to capture typical round fiat values. This range will
# evolve as Bitcoin’s purchasing power changes and may need to be updated in
# future versions of UTXOracle. From 2020 to 2025, we’ve found that most round fiat
# amounts fall within the range of 10^-6 to 10^6 BTC.
# Once the range and interval size are established, we generate arrays representing
# the edges of each interval and initialize a corresponding array of zeros to count
# how many transaction outputs fall into each bucket.
# 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_histogram_bins and add zero sats as the first bin
output_histogram_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_histogram_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_histogram_bins)
output_histogram_bin_counts = []
for n in range(0,number_of_bins):
output_histogram_bin_counts.append(float(0.0))
##############################################################################
# Step 6 - Load Transaction Data
##############################################################################
# This section of the algorithm is the most time-consuming because it reads every
# transaction from the range of blocks requested by the user. We typically need
# around 144 blocks, and since each block is roughly 1MB, this means processing
# about 144 MB of data.
# To improve efficiency, we request the raw binary block data and manually convert it
# into integers and strings. This requires defining functions that translate binary data
# into integers, such as read_varint and encode_varint. We also compute the txid
# manually, since binary Bitcoin blocks do not store it directly.
# After defining these functions, we loop through the list of required block hashes
# and extract transaction output amounts to place into histogram bins. We apply
# several filters to exclude transactions that are unlikely to reflect meaningful price
# information.
# Through years of testing we decided to filter out transactions containing: more than
# 5 inputs, more than 2 outputs, coinbase outputs, op_return outputs, large witness
# scripts, and same day inputs. If the output passes the filters, it is inserted into the
# histogram bin according to the bitcoin amount of the output.
print("\nLoading every transaction from every block",flush=True)
# #initialize output lists and variables
from struct import unpack
import binascii
todays_txids = set()
raw_outputs = []
block_heights_dec = []
block_times_dec = []
print_next = 0
block_num = 0
#shortcut for reading bytes of data from the block file
import struct
from math import log10
import hashlib
def read_varint(f):
i = f.read(1)
if not i:
return 0
i = i[0]
if i < 0xfd:
return i
elif i == 0xfd:
val = struct.unpack("<H", f.read(2))[0]
elif i == 0xfe:
val = struct.unpack("<I", f.read(4))[0]
else:
val = struct.unpack("<Q", f.read(8))[0]
return val
#shortcut for encoding variable size integers to bytes
from io import BytesIO
def encode_varint(i: int) -> bytes:
assert i >= 0
if i < 0xfd:
return i.to_bytes(1, 'little')
elif i <= 0xffff:
return b'\xfd' + i.to_bytes(2, 'little')
elif i <= 0xffffffff:
return b'\xfe' + i.to_bytes(4, 'little')
else:
return b'\xff' + i.to_bytes(8, 'little')
#shortcut for computing the txid because blk files don't store txids
def compute_txid(raw_tx_bytes: bytes) -> bytes:
stream = BytesIO(raw_tx_bytes)
# Read version
version = stream.read(4)
# Peek at marker/flag to detect SegWit
marker = stream.read(1)
flag = stream.read(1)
is_segwit = (marker == b'\x00' and flag == b'\x01')
if not is_segwit:
# Legacy tx: rewind and hash full raw tx
stream.seek(0)
stripped_tx = stream.read()
else:
# Start stripped tx with version
stripped_tx = bytearray()
stripped_tx += version
# Inputs
input_count = read_varint(stream)
stripped_tx += encode_varint(input_count)
for _ in range(input_count):
stripped_tx += stream.read(32) # prev txid
stripped_tx += stream.read(4) # vout index
script_len = read_varint(stream)
stripped_tx += encode_varint(script_len)
stripped_tx += stream.read(script_len)
stripped_tx += stream.read(4) # sequence
# Outputs
output_count = read_varint(stream)
stripped_tx += encode_varint(output_count)
for _ in range(output_count):
stripped_tx += stream.read(8) # value
script_len = read_varint(stream)
stripped_tx += encode_varint(script_len)
stripped_tx += stream.read(script_len)
# Skip witness data
for _ in range(input_count):
stack_count = read_varint(stream)
for _ in range(stack_count):
item_len = read_varint(stream)
stream.read(item_len)
# Locktime
stripped_tx += stream.read(4)
return hashlib.sha256(hashlib.sha256(stripped_tx).digest()).digest()[::-1]
# read in all blocks needed
for bh in block_hashes_needed:
block_num += 1
print_progress = block_num / len(block_hashes_needed) * 100
if print_progress > print_next and print_next < 100:
#print(f"{int(print_next)}% ", end="", flush=True)
print(f"{int(print_next)}%..",end="",flush=True)
print_next += 1
if print_next % 7 == 0:
print("\n", end="")
# Get raw block hex using RPC
raw_block_hex = Ask_Node(["getblock", bh, 0], False).decode().strip()
raw_block_bytes = binascii.unhexlify(raw_block_hex)
stream = BytesIO(raw_block_bytes)
# Read header (skip 80 bytes)
stream.read(80)
tx_count = read_varint(stream)
# loop through all txs in this block
txs_to_add = []
for tx_index in range(tx_count):
start_tx = stream.tell()
version = stream.read(4)
# Check for SegWit
marker_flag = stream.read(2)
is_segwit = marker_flag == b'\x00\x01'
if not is_segwit:
stream.seek(start_tx + 4)
# Read inputs
input_count = read_varint(stream)
inputs = []
has_op_return = False
witness_exceeds = False
is_coinbase = False
input_txids = []
for _ in range(input_count):
prev_txid = stream.read(32)
prev_index = stream.read(4)
script_len = read_varint(stream)
script = stream.read(script_len)
stream.read(4)
input_txids.append(prev_txid[::-1].hex())
if prev_txid == b'\x00' * 32 and prev_index == b'\xff\xff\xff\xff':
is_coinbase = True
inputs.append({"script": script})
#read outputs
output_count = read_varint(stream)
output_values = []
for _ in range(output_count):
value_sats = unpack("<Q", stream.read(8))[0]
script_len = read_varint(stream)
script = stream.read(script_len)
if script and script[0] == 0x6a:
has_op_return = True
value_btc = value_sats / 1e8
if 1e-5 < value_btc < 1e5:
output_values.append(value_btc)
# check witness data
if is_segwit:
for input_data in inputs:
stack_count = read_varint(stream)
total_witness_len = 0
for _ in range(stack_count):
item_len = read_varint(stream)
total_witness_len += item_len
stream.read(item_len)
if item_len > 500 or total_witness_len > 500:
witness_exceeds = True
#comput txid
stream.read(4)
end_tx = stream.tell()
stream.seek(start_tx)
raw_tx = stream.read(end_tx - start_tx)
txid = compute_txid(raw_tx)
todays_txids.add(txid.hex())
#check same day tx
is_same_day_tx = any(itxid in todays_txids for itxid in input_txids)
# apply filter and add output to bell curve
if (input_count <= 5 and output_count == 2 and not is_coinbase and
not has_op_return and not witness_exceeds and not is_same_day_tx):
for amount in output_values:
amount_log = log10(amount)
percent_in_range = (amount_log - first_bin_value) / range_bin_values
bin_number_est = int(percent_in_range * number_of_bins)
while output_histogram_bins[bin_number_est] <= amount:
bin_number_est += 1
bin_number = bin_number_est - 1
output_histogram_bin_counts[bin_number] += 1.0
txs_to_add.append(amount)
# add outputs to raw outputs
if len(txs_to_add) > 0:
bkh = block_nums_needed[block_num - 1]
tm = block_times_needed[block_num - 1]
for amt in txs_to_add:
raw_outputs.append(amt)
block_heights_dec.append(bkh)
block_times_dec.append(tm)
print("100%",flush=True)
print("\t\t\t\t\t\t95% done",flush=True)
##############################################################################
# Step 7 - Remove Round Bitcoin Amounts
##############################################################################
# Although most transaction amounts are related to fiat purchasing power, spending
# round Bitcoin amounts is still common. This makes it difficult to determine whether
# a histogram interval is truly capturing a round fiat value or simply reflecting a round
# BTC amount.
# We can't completely remove round BTC amounts, because when the fiat price of
# Bitcoin is near a round number, round BTC and round fiat values can overlap.
# Instead of removing these intervals, we smooth them by averaging the values of
# the histogram bins directly above and below the round BTC amounts.
# After smoothing, we normalize the histogram by dividing each bin by the total sum.
# This converts the histogram into percentages rather than raw counts. Percentages
# are more stable across days with unusually high or low transaction volume, making
# the resulting signal more consistent.
# print update
print("\nFinding prices and rendering plot",flush=True)
print("0%..",end="",flush=True)
#remove outputs below 10k sat (increased from 1k sat in v6)
for n in range(0,201):
output_histogram_bin_counts[n]=0
#remove outputs above ten btc
for n in range(1601,len(output_histogram_bin_counts)):
output_histogram_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_histogram_bin_counts[r+1]
amount_below = output_histogram_bin_counts[r-1]
output_histogram_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_histogram_bin_counts[n]
#normalize the curve by dividing by it's sum and removing extreme values
for n in range(201,1601):
output_histogram_bin_counts[n] /= curve_sum
#remove extremes (0.008 chosen by historical testing)
if output_histogram_bin_counts[n] > 0.008:
output_histogram_bin_counts[n] = 0.008
#print update
print("20%..",end="",flush=True)
##############################################################################
# Step 8 - Construct the Price Finding Stencil
##############################################################################
# Users don’t just send round fiat amounts, they also tend to send them in
# predictable proportions depending on the amount. For example, users spend $100
# far more often than they send $10,000. We use this proportionality both to lock
# onto round fiat amounts and to determine which amount is which.
# Through historical testing, we’ve measured the average histogram bin counts for
# each common round fiat amount and hard-code these values into what we call a
# price-finding stencil. The most common and heavily weighted amount in the stencil
# is $100. Less frequently used fiat amounts are assigned lower weights, since we
# expect to see them transacted less often.
# Even when users are not transacting at a perfectly round fiat amount, their
# behavior is still influenced by Bitcoin’s long-term rise in purchasing power. For
# example, if we plot a bell curve of output amounts, we find that the center of the
# curve shifts toward smaller BTC amounts over time.
# This provides additional information we can use. We first apply a smooth stencil to
# estimate a center of gravity for the price within a broad 20% range. Then, we apply
# a spike stencil based on perfectly round USD amounts to refine the price estimate
# to within about 0.5%.
# Parameters