robosats/api/utils.py

218 lines
6.8 KiB
Python

import json
import os
import numpy as np
import requests
import ring
from decouple import config
from api.models import Order
def get_tor_session():
session = requests.session()
# Tor uses the 9050 port as the default socks port
session.proxies = {'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'}
return session
def bitcoind_rpc(method, params=None):
"""
Makes a RPC call to bitcoin core daemon
:param method: RPC method to call
:param params: list of params required by the calling RPC method
:return:
"""
BITCOIND_RPCURL = config('BITCOIND_RPCURL')
BITCOIND_RPCUSER = config('BITCOIND_RPCUSER')
BITCOIND_RPCPASSWORD = config('BITCOIND_RPCPASSWORD')
if params is None:
params = []
payload = json.dumps(
{
"jsonrpc": "2.0",
"id": "robosats",
"method": method,
"params": params
}
)
return requests.post(BITCOIND_RPCURL, auth=(BITCOIND_RPCUSER, BITCOIND_RPCPASSWORD), data=payload).json()['result']
def validate_onchain_address(address):
"""
Validates an onchain address
"""
try:
validation = bitcoind_rpc('validateaddress', [address])
if not validation['isvalid']:
return False, {"bad_address": validation['error']}
except:
# TODO: log the exception ?
return False, {"bad_address": 'Unable to validate address, check bitcoind backend'}
return True, None
market_cache = {}
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
def get_exchange_rates(currencies):
"""
Params: list of currency codes.
Checks for exchange rates in several public APIs.
Returns the median price list.
"""
session = get_tor_session()
APIS = config("MARKET_PRICE_APIS",
cast=lambda v: [s.strip() for s in v.split(",")])
api_rates = []
for api_url in APIS:
try: # If one API is unavailable pass
if "blockchain.info" in api_url:
blockchain_prices = session.get(api_url).json()
blockchain_rates = []
for currency in currencies:
try: # If a currency is missing place a None
blockchain_rates.append(
float(blockchain_prices[currency]["last"]))
except:
blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates)
elif "yadio.io" in api_url:
yadio_prices = session.get(api_url).json()
yadio_rates = []
for currency in currencies:
try:
yadio_rates.append(float(
yadio_prices["BTC"][currency]))
except:
yadio_rates.append(np.nan)
api_rates.append(yadio_rates)
except:
pass
if len(api_rates) == 0:
return None # Wops there is not API available!
exchange_rates = np.array(api_rates)
median_rates = np.nanmedian(exchange_rates, axis=0)
return median_rates.tolist()
def get_lnd_version():
# If dockerized, return LND_VERSION envvar used for docker image.
# Otherwise it would require LND's version.grpc libraries...
try:
lnd_version = config("LND_VERSION")
return lnd_version
except:
pass
# If not dockerized and LND is local, read from CLI
try:
stream = os.popen("lnd --version")
lnd_version = stream.read()[:-1]
return lnd_version
except:
return ""
robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
commit = os.popen('git log -n 1 --pretty=format:"%H"')
commit_hash = commit.read()
# .git folder is included in .dockerignore. But automatic build will drop in a commit_sha.txt file on root
if commit_hash == None or commit_hash =="":
with open("commit_sha.txt") as f:
commit_hash = f.read()
return commit_hash
premium_percentile = {}
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
queryset = Order.objects.filter(
currency=order.currency, status=Order.Status.PUB, type=order.type).exclude(id=order.id)
print(len(queryset))
if len(queryset) <= 1:
return 0.5
amount = order.amount if not order.has_range else order.max_amount
order_rate = float(order.last_satoshis) / float(amount)
rates = []
for similar_order in queryset:
similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount
rates.append(
float(similar_order.last_satoshis) / float(similar_order_amount))
rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates), 2)
def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=False):
"""Very close to numpy.percentile, but it supports weights.
NOTE: quantiles should be in [0, 1]!
:param values: numpy.array with data
:param quantiles: array-like with many quantiles needed. For weighted median 0.5
:param sample_weight: array-like of the same length as `array`
:param values_sorted: bool, if True, then will avoid sorting of
initial array assuming array is already sorted
:return: numpy.array with computed quantiles.
"""
values = np.array(values)
quantiles = np.array(quantiles)
if sample_weight is None:
sample_weight = np.ones(len(values))
sample_weight = np.array(sample_weight)
assert np.all(quantiles >= 0) and np.all(quantiles <= 1), \
'quantiles should be in [0, 1]'
if not values_sorted:
sorter = np.argsort(values)
values = values[sorter]
sample_weight = sample_weight[sorter]
weighted_quantiles = np.cumsum(sample_weight) - 0.5 * sample_weight
weighted_quantiles -= weighted_quantiles[0]
weighted_quantiles /= weighted_quantiles[-1]
return np.interp(quantiles, weighted_quantiles, values)
def compute_avg_premium(queryset):
premiums = []
volumes = []
# We exclude BTC, as LN <-> BTC swap premiums should not be mixed with FIAT.
for tick in queryset.exclude(currency=1000):
premiums.append(float(tick.premium))
volumes.append(float(tick.volume))
total_volume = sum(volumes)
# weighted_median_premium is the weighted median of the premiums by volume
if len(premiums) > 0 and len(volumes)>0:
weighted_median_premium = weighted_median(values=premiums,
sample_weight=volumes,
quantiles=0.5,
values_sorted=False)
else:
weighted_median_premium = 0.0
return weighted_median_premium, total_volume