2022-12-20 11:54:56 +02:00
#!/usr/bin/python3
2023-01-01 13:40:47 +02:00
'''
Fetch BTCUSD OHLC data from few market places and serve it forward with simple json api .
2022-12-20 11:54:56 +02:00
2023-01-01 13:40:47 +02:00
Creates : . / btc_ohlc . db
serves : localhost : 5000 / [ t ] and / serverkey
Authentication via auth header with signatures
'''
import math
import json
import os
import time
import sqlite3
import binascii
import threading
2022-12-28 20:36:44 +02:00
from hashlib import sha256
2023-01-01 13:40:47 +02:00
import requests
import ecdsa
import krakenex
2022-12-29 17:59:25 +02:00
from flask import Flask , jsonify , request
2023-01-01 13:40:47 +02:00
#from Cryptodome.Cipher import AES
2022-12-20 11:54:56 +02:00
2023-01-01 13:40:47 +02:00
DATABASE = " btc_ohlc.db "
2022-12-28 01:10:26 +02:00
app = Flask ( __name__ )
2022-12-28 22:49:37 +02:00
## Add your public key here
user_publickeys = {
2022-12-29 22:09:26 +02:00
" user1 " : ' f1debc13fb21fe0eee54525aa4f8aae5733b201c755edaa55f8893c90aa375b261a62eaa3110651ac5d7705d402581256a37508b0a1ca28bd919ea44710d9c88 '
2022-12-28 22:49:37 +02:00
}
2022-12-29 22:09:26 +02:00
## Generate the ECDSA keys for this instance
print ( " Generating ECDSA keys for this instance... just wait a bit... " )
server_private_key = ecdsa . SigningKey . generate ( curve = ecdsa . SECP256k1 )
server_public_key = server_private_key . get_verifying_key ( )
# We need the hexadecimal form for sharing over http/json
server_public_key_hex = binascii . hexlify ( server_public_key . to_string ( ) ) . decode ( ' utf-8 ' )
2022-12-29 17:59:25 +02:00
2022-12-28 01:10:26 +02:00
database_lock = threading . Lock ( )
# Empty response json
empty_dict = { " exchange " : " " , " timestamp " : 0 , " open " : 0 , " high " : 0 , " low " : 0 , " close " : 0 , " volume_quote " : 0 , " volume_base " : 0 , " trades " : 0 }
empty_json = json . dumps ( empty_dict )
2022-12-20 11:54:56 +02:00
2023-01-01 13:40:47 +02:00
def check_database ( ) :
"""
Check the database for the ' ohlc ' table .
If the database file or the table does not exist , create them .
"""
if not os . path . exists ( DATABASE ) :
new_db = sqlite3 . connect ( DATABASE )
new_db . execute ( """ \
2022-12-20 11:54:56 +02:00
CREATE TABLE ohlc (
id INTEGER PRIMARY KEY ,
exchange TEXT NOT NULL ,
timestamp INTEGER NOT NULL ,
open REAL NOT NULL ,
high REAL NOT NULL ,
low REAL NOT NULL ,
close REAL NOT NULL ,
volume_quote REAL NOT NULL ,
volume_base REAL NOT NULL ,
trades INTEGER NOT NULL ) """ )
2023-01-01 13:40:47 +02:00
new_db . commit ( )
new_db . close ( )
new_db = sqlite3 . connect ( DATABASE )
2022-12-20 11:54:56 +02:00
# Check if the table exists
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
table_exists = False
2023-01-01 13:40:47 +02:00
cursor = new_db . execute ( " PRAGMA table_info(ohlc) " )
2022-12-20 11:54:56 +02:00
for row in cursor :
table_exists = True
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
# Create the table if it doesn't exist
if not table_exists :
2023-01-01 13:40:47 +02:00
new_db . execute ( """ \
2022-12-20 11:54:56 +02:00
CREATE TABLE ohlc (
id INTEGER PRIMARY KEY ,
exchange TEXT NOT NULL ,
timestamp INTEGER NOT NULL ,
open REAL NOT NULL ,
high REAL NOT NULL ,
low REAL NOT NULL ,
close REAL NOT NULL ,
volume_quote REAL NOT NULL ,
volume_base REAL NOT NULL ,
trades INTEGER NOT NULL ) """ )
2023-01-01 13:40:47 +02:00
new_db . commit ( )
2022-12-20 11:54:56 +02:00
def fetch_kraken ( ) :
2023-01-01 13:40:47 +02:00
"""
Fetch BTCUSD OHLC data from Kraken in json .
Returns :
str : 5 min OHLC data in JSON format .
"""
2022-12-20 11:54:56 +02:00
kraken = krakenex . API ( )
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
response = kraken . query_public ( ' OHLC ' , { ' pair ' : ' BTCUSD ' , ' interval ' : 240 } )
ohlc_data = response [ ' result ' ] [ ' XXBTZUSD ' ]
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
candle_stick_data = {
' exchange ' : ' kraken ' ,
' timestamp ' : ohlc_data [ 1 ] [ 0 ] ,
' open ' : ohlc_data [ 0 ] [ 1 ] ,
' high ' : max ( item [ 2 ] for item in ohlc_data ) ,
' low ' : min ( item [ 3 ] for item in ohlc_data ) ,
' close ' : ohlc_data [ - 1 ] [ 4 ] ,
' volume_quote ' : sum ( float ( item [ 5 ] ) for item in ohlc_data ) ,
' volume_base ' : sum ( float ( item [ 6 ] ) for item in ohlc_data ) ,
' trades ' : sum ( item [ 7 ] for item in ohlc_data ) ,
}
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
kraken_json = json . dumps ( candle_stick_data , indent = 2 )
#print("Kraken: OK")
#print(kraken_json)
return kraken_json
def fetch_bitstamp ( ) :
2023-01-01 13:40:47 +02:00
"""
Fetch Bitstamp data ja serve it as json .
Returns :
str : 5 min OHLC data in JSON format .
"""
2022-12-20 11:54:56 +02:00
response = requests . get ( " https://www.bitstamp.net/api/v2/ohlc/btcusd/?step=300&limit=1 " )
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
if response . status_code == 200 : # check if the request was successful
bitstamp_data = response . json ( )
ohlc_data = bitstamp_data [ " data " ] [ " ohlc " ]
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
candle_stick_data = {
' exchange ' : ' bitstamp ' ,
' timestamp ' : int ( ohlc_data [ 0 ] [ ' timestamp ' ] ) ,
' open ' : float ( ohlc_data [ 0 ] [ ' open ' ] ) ,
' high ' : float ( ohlc_data [ 0 ] [ ' high ' ] ) ,
' low ' : float ( ohlc_data [ 0 ] [ ' low ' ] ) ,
' close ' : float ( ohlc_data [ 0 ] [ ' close ' ] ) ,
' volume_quote ' : float ( ohlc_data [ 0 ] [ ' volume ' ] ) ,
' volume_base ' : 0 , # not provided by Bitstamp API
' trades ' : 0 , # not provided by Bitstamp API
}
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
bitstamp_json = json . dumps ( candle_stick_data , indent = 2 )
#print("Bitstamp: OK")
2022-12-27 20:39:36 +02:00
# print(bitstamp_json)
2022-12-20 11:54:56 +02:00
return bitstamp_json
else :
print ( f " Error fetching data from Bitstamp API: { response . status_code } " )
2022-12-27 20:39:36 +02:00
return empty_json
2022-12-20 11:54:56 +02:00
def fetch_bitfinex ( ) :
2023-01-01 13:40:47 +02:00
"""
Bitfinex
Returns :
str : 5 min OHLC data in JSON format .
"""
2022-12-20 11:54:56 +02:00
response = requests . get ( " https://api-pub.bitfinex.com/v2/candles/trade:5m:tBTCUSD/last " )
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
if response . status_code == 200 : # check if the request was successful
ohlc_data = response . json ( )
candle_stick_data = {
' exchange ' : ' bitfinex ' ,
' timestamp ' : ohlc_data [ 0 ] ,
' open ' : ohlc_data [ 1 ] ,
' high ' : ohlc_data [ 2 ] ,
' low ' : ohlc_data [ 3 ] ,
' close ' : ohlc_data [ 4 ] ,
' volume_quote ' : ohlc_data [ 5 ] ,
' volume_base ' : 0 , # not provided by Bitfinex API
' trades ' : 0 , # not provided by Bitfinex API
}
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
bitfinex_json = json . dumps ( candle_stick_data , indent = 2 )
#print("Bitfinex: OK")
#print(bitfinex_json)
return bitfinex_json
else :
print ( f " Error fetching data from Bitfinex API: { response . status_code } " )
2022-12-27 20:39:36 +02:00
return empty_json
2022-12-20 11:54:56 +02:00
def fetch_gemini ( ) :
2023-01-01 13:40:47 +02:00
"""
Fetch BTCUSD OHLC data from Gemini
Returns :
str : 5 min OHLC data in JSON format .
"""
2022-12-20 11:54:56 +02:00
response = requests . get ( " https://api.gemini.com/v2/candles/btcusd/5m " )
2023-01-01 13:40:47 +02:00
2022-12-20 11:54:56 +02:00
if response . status_code == 200 : # check if the request was successful
gemini_ohlc = response . json ( )
candle_stick_data = {
' exchange ' : ' gemini ' ,
' timestamp ' : gemini_ohlc [ 0 ] [ 0 ] ,
' open ' : gemini_ohlc [ 0 ] [ 1 ] ,
' high ' : gemini_ohlc [ 0 ] [ 2 ] ,
' low ' : gemini_ohlc [ 0 ] [ 3 ] ,
' close ' : gemini_ohlc [ 0 ] [ 4 ] ,
' volume_quote ' : 0 , # not provided by Gemini API
' volume_base ' : gemini_ohlc [ 0 ] [ 5 ] ,
' trades ' : 0 , # not provided by Gemini API
}
gemini_json = json . dumps ( candle_stick_data , indent = 2 )
#print("Gemini: OK")
#print(gemini_json)
return gemini_json
else :
print ( f " Error fetching data from Gemini API: { response . status_code } " )
2022-12-27 20:39:36 +02:00
return empty_json
2022-12-20 11:54:56 +02:00
2022-12-28 15:58:55 +02:00
def fetch_bybit ( ) :
2023-01-01 13:40:47 +02:00
"""
Fetch BTCUSD OHLC data from Bybit
Returns :
str : 5 min OHLC data in JSON format .
"""
2022-12-28 15:58:55 +02:00
base_url = ' https://api.bybit.com/v2/public/kline/list?symbol=BTCUSD&interval=5&from= '
current_unixtime = int ( time . time ( ) )
last_minute = math . floor ( current_unixtime / 60 )
last_minute_unixtime = str ( last_minute * 60 - 300 )
query_url = ' ' . join ( [ base_url , last_minute_unixtime ] )
response = requests . get ( query_url )
if response . status_code == 200 : # check if the request was successful
bybit_ohlc = response . json ( )
candle_stick_data = {
' exchange ' : ' bybit ' ,
' timestamp ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' open_time ' ] ,
' open ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' open ' ] ,
' high ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' high ' ] ,
' low ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' low ' ] ,
' close ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' close ' ] ,
' volume_quote ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' volume ' ] ,
' volume_base ' : bybit_ohlc [ ' result ' ] [ 0 ] [ ' turnover ' ] ,
' trades ' : 0
}
bybit_json = json . dumps ( candle_stick_data , indent = 2 )
return bybit_json
else :
print ( f " Error fetching data from Bybit API: { response . status_code } " )
return empty_json
2022-12-20 17:20:40 +02:00
def write_dict_to_database ( in_dict , connection ) :
2023-01-01 13:40:47 +02:00
"""
Writes given dict to given database .
Arguments : dict , db . connection ( )
Uses shared global database_lock .
"""
2022-12-20 17:20:40 +02:00
cursor = connection . cursor ( )
2022-12-28 15:58:55 +02:00
# use placeholders for the values in the insert statement
insert_query = " insert into ohlc (exchange, timestamp, open, high, low, close, volume_quote, volume_base, trades) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
2022-12-20 11:54:56 +02:00
2023-01-01 13:40:47 +02:00
values = ( in_dict [ ' exchange ' ] ,
in_dict [ ' timestamp ' ] ,
in_dict [ ' open ' ] ,
in_dict [ ' high ' ] ,
in_dict [ ' low ' ] ,
in_dict [ ' close ' ] ,
in_dict [ ' volume_quote ' ] ,
in_dict [ ' volume_base ' ] ,
2022-12-20 11:54:56 +02:00
in_dict [ ' trades ' ] )
2022-12-20 17:20:40 +02:00
## apply lock while writing to database
with database_lock :
cursor . execute ( insert_query , values )
connection . commit ( )
2022-12-20 11:54:56 +02:00
def get_the_data ( ) :
2023-01-01 13:40:47 +02:00
"""
Creates infinite While True loop to fetch OHLC data and save it to database .
"""
while True :
ohlc_db = sqlite3 . connect ( DATABASE )
write_dict_to_database ( json . loads ( fetch_kraken ( ) ) , ohlc_db )
write_dict_to_database ( json . loads ( fetch_bitfinex ( ) ) , ohlc_db )
write_dict_to_database ( json . loads ( fetch_bitstamp ( ) ) , ohlc_db )
write_dict_to_database ( json . loads ( fetch_gemini ( ) ) , ohlc_db )
write_dict_to_database ( json . loads ( fetch_bybit ( ) ) , ohlc_db )
ohlc_db . close ( )
2022-12-28 01:10:26 +02:00
print ( " fetches done at " , time . time ( ) , " sleeping now for 290 " )
2022-12-20 17:20:40 +02:00
time . sleep ( 290 )
2022-12-28 22:49:37 +02:00
def check_auth ( text , signature ) :
2023-01-01 13:40:47 +02:00
"""
Check signatures against known public keys
Arguments : text , signature
Reads : Global public user_publickeys dict .
Returns : True / False
"""
2022-12-29 17:59:25 +02:00
## Make bytes-object from given signature
2022-12-28 22:49:37 +02:00
sig_bytes = bytes . fromhex ( signature )
## We will iterate over all user keys to determ who is we are talking to and should they have access
for key , value in user_publickeys . items ( ) :
2022-12-29 17:59:25 +02:00
## Create bytes-object from the public in 'value' variable
## and use it to create VerifyingKey (vk)
public_key_bytes = bytes . fromhex ( value )
2023-01-01 13:40:47 +02:00
verifying_key = ecdsa . VerifyingKey . from_string ( public_key_bytes , curve = ecdsa . SECP256k1 )
2022-12-29 17:59:25 +02:00
try :
2023-01-01 13:40:47 +02:00
verifying_key . verify ( sig_bytes , bytes ( text , ' utf-8 ' ) )
2022-12-28 22:49:37 +02:00
print ( ' user is ' , key )
2022-12-29 22:09:26 +02:00
2022-12-29 17:59:25 +02:00
return True
except ecdsa . BadSignatureError :
return False
2022-12-28 22:49:37 +02:00
2022-12-28 01:10:26 +02:00
@app.route ( ' / ' )
def get_data ( ) :
2023-01-01 13:40:47 +02:00
"""
Serve the data from the database . Limit the responses by given timestamp .
The pretty thing is under consideration . . .
"""
2022-12-28 01:10:26 +02:00
# Get the time (t) argument from the url"
query_timestamp = request . args . get ( ' t ' )
2022-12-28 22:49:37 +02:00
# Should we make output pretty for curl users?
2022-12-28 01:10:26 +02:00
query_pretty = request . args . get ( ' pretty ' )
2022-12-28 22:49:37 +02:00
# Authentication header, signatured the query with private key of a user
signature = request . headers . get ( ' auth ' )
get_url = request . url
if not check_auth ( get_url , signature ) :
2022-12-29 17:59:25 +02:00
return ' Access denied! Check your keys, maybe. ' , 403
2022-12-28 22:49:37 +02:00
2023-01-01 13:40:47 +02:00
with database_lock :
btc_db = sqlite3 . connect ( DATABASE )
if query_timestamp :
rows = btc_db . execute ( " SELECT exchange, timestamp, open, high, low, close FROM ohlc WHERE timestamp > ? ORDER BY timestamp " , ( query_timestamp , ) ) . fetchall ( )
else :
rows = btc_db . execute ( ' SELECT exchange, timestamp, open, high, low, close FROM ohlc ORDER BY timestamp ' ) . fetchall ( )
query_timestamp = 0
2022-12-28 20:36:44 +02:00
2023-01-01 13:40:47 +02:00
data = {
" timestamp " : time . time ( ) ,
" rows " : rows
}
2022-12-27 20:39:36 +02:00
2022-12-28 20:36:44 +02:00
# make sha256 checksum and append it to the data object
data_shasum = sha256 ( json . dumps ( data ) . encode ( ' utf-8 ' ) ) . hexdigest ( )
updated_data = { " shasum " : data_shasum }
updated_data . update ( data )
data = updated_data
2023-01-01 13:40:47 +02:00
2022-12-29 22:09:26 +02:00
# sign the response
signature = server_private_key . sign ( json . dumps ( data ) . encode ( ' utf-8 ' ) )
signature_hex = binascii . hexlify ( signature ) . decode ( ' utf-8 ' )
data [ ' signature ' ] = signature_hex
2022-12-28 20:36:44 +02:00
2022-12-28 01:10:26 +02:00
if query_pretty :
2023-01-01 13:40:47 +02:00
response = json . dumps ( data , indent = 2 , separators = ( ' ; \n ' , ' : ' ) )
2022-12-20 17:20:40 +02:00
else :
2022-12-28 01:10:26 +02:00
response = json . dumps ( data )
return response , 200 , { ' Content-Type ' : ' application/json ' }
2022-12-29 17:59:25 +02:00
@app.route ( ' /serverkey ' )
def give_serverkey ( ) :
2023-01-01 13:40:47 +02:00
"""
Serve the public keys of this instace to the world .
"""
2022-12-29 17:59:25 +02:00
## This endpoint also under Authentication?
signature = request . headers . get ( ' auth ' )
get_url = request . url
if not check_auth ( get_url , signature ) :
return ' Access denied! Check your keys, maybe. ' , 403
2022-12-29 22:09:26 +02:00
return jsonify ( { ' public_key ' : server_public_key_hex } )
2022-12-29 17:59:25 +02:00
2022-12-28 01:10:26 +02:00
if __name__ == ' __main__ ' :
# Make sanity checks for the database
2023-01-01 13:40:47 +02:00
check_database ( )
2022-12-28 01:10:26 +02:00
# Start the data fetching backend process
fetch_thread = threading . Thread ( target = get_the_data )
fetch_thread . daemon = True
fetch_thread . start ( )
2023-01-01 13:40:47 +02:00
2022-12-28 01:10:26 +02:00
# Start the Flask app
app . run ( )