pluto/handler/lib/money.py

476 lines
17 KiB
Python

# Twisted imports
from twisted.internet.task import LoopingCall
from twisted.internet.defer import inlineCallbacks
# Other library imports
from pycoingecko import CoinGeckoAPI
from forex_python.converter import CurrencyRates
import urllib3
import logging
from elasticsearch import Elasticsearch
from datetime import datetime
# Project imports
from settings import settings
import util
# TODO: secure ES traffic properly
urllib3.disable_warnings()
tracer = logging.getLogger("elasticsearch")
tracer.setLevel(logging.CRITICAL)
tracer = logging.getLogger("elastic_transport.transport")
tracer.setLevel(logging.CRITICAL)
class Money(util.Base):
"""
Generic class for handling money-related matters that aren't Revolut or Agora.
"""
def __init__(self):
"""
Initialise the Money object.
Set the logger.
Initialise the CoinGecko API.
"""
super().__init__()
self.cr = CurrencyRates()
self.cg = CoinGeckoAPI()
if settings.ES.Enabled == "1":
self.es = Elasticsearch(
f"https://{settings.ES.Host}:9200",
verify_certs=False,
basic_auth=(settings.ES.Username, settings.ES.Pass),
# ssl_assert_fingerprint=("6b264fd2fd107d45652d8add1750a8a78f424542e13b056d0548173006260710"),
ca_certs="certs/ca.crt",
)
def run_checks_in_thread(self):
"""
Run all the balance checks that output into ES in another thread.
"""
self.get_total()
self.get_remaining()
self.get_profit()
self.get_profit(True)
self.get_open_trades_usd()
self.get_total_remaining()
self.get_total_with_trades()
def setup_loops(self):
"""
Set up the LoopingCalls to get the balance so we have data in ES.
"""
if settings.ES.Enabled == "1":
self.lc_es_checks = LoopingCall(self.run_checks_in_thread)
delay = int(settings.ES.RefreshSec)
self.lc_es_checks.start(delay)
self.agora.es = self.es
def write_to_es(self, msgtype, cast):
if settings.ES.Enabled == "1":
cast["type"] = msgtype
cast["ts"] = str(datetime.now().isoformat())
cast["xtype"] = "tx"
self.es.index(index=settings.ES.Index, document=cast)
def lookup_rates(self, platform, ads, rates=None):
"""
Lookup the rates for a list of public ads.
"""
if not rates:
rates = self.cg.get_price(
ids=["monero", "bitcoin"],
vs_currencies=self.markets.get_all_currencies(platform),
)
# Set the price based on the asset
for ad in ads:
if ad[4] == "XMR":
coin = "monero"
elif ad[4] == "BTC":
coin = "bitcoin" # No s here
currency = ad[5]
base_currency_price = rates[coin][currency.lower()]
price = float(ad[2])
rate = round(price / base_currency_price, 2)
ad.append(rate)
# TODO: sort?
return sorted(ads, key=lambda x: x[2])
def get_rates_all(self):
"""
Get all rates that pair with USD.
:return: dictionary of USD/XXX rates
:rtype: dict
"""
rates = self.cr.get_rates("USD")
return rates
def get_acceptable_margins(self, platform, currency, amount):
"""
Get the minimum and maximum amounts we would accept a trade for.
:param currency: currency code
:param amount: amount
:return: (min, max)
:rtype: tuple
"""
sets = util.get_settings(platform)
rates = self.get_rates_all()
if currency == "USD":
min_amount = amount - float(sets.AcceptableUSDMargin)
max_amount = amount + float(sets.AcceptableUSDMargin)
return (min_amount, max_amount)
amount_usd = amount / rates[currency]
min_usd = amount_usd - float(sets.AcceptableUSDMargin)
max_usd = amount_usd + float(sets.AcceptableUSDMargin)
min_local = min_usd * rates[currency]
max_local = max_usd * rates[currency]
return (min_local, max_local)
def get_minmax(self, platform, asset, currency):
sets = util.get_settings(platform)
rates = self.get_rates_all()
if currency not in rates and not currency == "USD":
self.log.error(f"Can't create ad without rates: {currency}")
return
if asset == "XMR":
min_usd = float(sets.MinUSDXMR)
max_usd = float(sets.MaxUSDXMR)
elif asset == "BTC":
min_usd = float(sets.MinUSDBTC)
max_usd = float(sets.MaxUSDBTC)
if currency == "USD":
min_amount = min_usd
max_amount = max_usd
else:
min_amount = rates[currency] * min_usd
max_amount = rates[currency] * max_usd
return (min_amount, max_amount)
def to_usd(self, amount, currency):
if currency == "USD":
return float(amount)
else:
rates = self.get_rates_all()
return float(amount) / rates[currency]
def multiple_to_usd(self, currency_map):
"""
Convert multiple curencies to USD while saving API calls.
"""
rates = self.get_rates_all()
cumul = 0
for currency, amount in currency_map.items():
if currency == "USD":
cumul += float(amount)
else:
cumul += float(amount) / rates[currency]
return cumul
# TODO: move to money
@inlineCallbacks
def get_profit(self, trades=False):
"""
Check how much total profit we have made.
:return: profit in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
if not total_usd:
return False
if trades:
trades_usd = yield self.get_open_trades_usd()
total_usd += trades_usd
profit = total_usd - float(settings.Money.BaseUSD)
if trades:
cast_es = {
"profit_trades_usd": profit,
}
else:
cast_es = {
"profit_usd": profit,
}
self.write_to_es("get_profit", cast_es)
return profit
@inlineCallbacks
def get_total_usd(self):
"""
Get total USD in all our accounts, bank and trading.
:return: value in USD
:rtype float:
"""
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
agora_wallet_btc = yield self.agora.api.wallet_balance()
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
if not agora_wallet_xmr["success"]:
return False
if not agora_wallet_btc["success"]:
return False
if not lbtc_wallet_btc["success"]:
return False
if not agora_wallet_xmr["response"]:
return False
if not agora_wallet_btc["response"]:
return False
if not lbtc_wallet_btc["response"]:
return False
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Convert the LBTC BTC total to USD
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd_lbtc = total_usd_lbtc_btc
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
cast_es = {
"price_usd": total_usd,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_usd_lbtc_btc": total_usd_lbtc_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"total_btc_lbtc": total_btc_lbtc,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_sinks_usd": total_sinks_usd,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total_usd", cast_es)
return total_usd
# TODO: possibly refactor this into smaller functions which don't return as much stuff
# check if this is all really needed in the corresponding withdraw function
@inlineCallbacks
def get_total(self):
"""
Get all the values corresponding to the amount of money we hold.
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC))
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
"""
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = yield self.agora.api.wallet_balance_xmr()
agora_wallet_btc = yield self.agora.api.wallet_balance()
lbtc_wallet_btc = yield self.lbtc.api.wallet_balance()
if not agora_wallet_xmr["success"]:
return False
if not agora_wallet_btc["success"]:
return False
if not lbtc_wallet_btc["success"]:
return False
if not agora_wallet_xmr["response"]:
return False
if not agora_wallet_btc["response"]:
return False
if not lbtc_wallet_btc["response"]:
return False
total_xmr_agora = agora_wallet_xmr["response"]["data"]["total"]["balance"]
total_btc_agora = agora_wallet_btc["response"]["data"]["total"]["balance"]
total_btc_lbtc = lbtc_wallet_btc["response"]["data"]["total"]["balance"]
# Get the XMR -> USD exchange rate
xmr_usd = self.cg.get_price(ids="monero", vs_currencies=["USD"])
# Get the BTC -> USD exchange rate
btc_usd = self.cg.get_price(ids="bitcoin", vs_currencies=["USD"])
# Convert the Agora XMR total to USD
total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"]
# Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"]
# Convert the LBTC BTC total to USD
total_usd_lbtc_btc = float(total_btc_lbtc) * btc_usd["bitcoin"]["usd"]
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
total_usd_lbtc = total_usd_lbtc_btc
total_usd = total_usd_agora + total_usd_lbtc + total_sinks_usd
total_btc_usd = total_usd_agora_btc + total_usd_lbtc_btc
total_xmr_usd = total_usd_agora_xmr
total_xmr = total_xmr_agora
total_btc = total_btc_lbtc + total_btc_agora
# Convert the total USD price to GBP and SEK
rates = self.get_rates_all()
price_sek = rates["SEK"] * total_usd
price_usd = total_usd
price_gbp = rates["GBP"] * total_usd
cast = (
(
price_sek,
price_usd,
price_gbp,
), # Total prices in our 3 favourite currencies
(
total_xmr_usd,
total_btc_usd,
), # Total USD balance in only Agora
(total_xmr, total_btc),
) # Total XMR and BTC balance in Agora
cast_es = {
"price_sek": price_sek,
"price_usd": price_usd,
"price_gbp": price_gbp,
"total_usd_agora_xmr": total_usd_agora_xmr,
"total_usd_agora_btc": total_usd_agora_btc,
"total_usd_lbtc_btc": total_usd_lbtc_btc,
"total_xmr_agora": total_xmr_agora,
"total_btc_agora": total_btc_agora,
"total_btc_lbtc": total_btc_lbtc,
"xmr_usd": xmr_usd["monero"]["usd"],
"btc_usd": btc_usd["bitcoin"]["usd"],
"total_sinks_usd": total_sinks_usd,
"total_usd_agora": total_usd_agora,
}
self.write_to_es("get_total", cast_es)
return cast
@inlineCallbacks
def get_remaining(self):
"""
Check how much profit we need to make in order to withdraw.
:return: profit remaining in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
if not total_usd:
return False
withdraw_threshold = float(settings.Money.BaseUSD) + float(settings.Money.WithdrawLimit)
remaining = withdraw_threshold - total_usd
cast_es = {
"remaining_usd": remaining,
}
self.write_to_es("get_remaining", cast_es)
return remaining
def open_trades_usd_parse_dash(self, platform, dash, rates):
cumul_usd = 0
for contact_id, contact in dash.items():
# We need created at in order to look up the historical prices
created_at = contact["data"]["created_at"]
# Reformat the date how CoinGecko likes
# 2022-05-02T11:17:14+00:00
if "+" in created_at:
date_split = created_at.split("+")
date_split[1].replace(".", "")
date_split[1].replace(":", "")
created_at = "+".join(date_split)
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
else:
date_parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%fZ")
date_formatted = date_parsed.strftime("%d-%m-%Y")
# Get the historical rates for the right asset, extract the price
if platform == "agora":
asset = contact["data"]["advertisement"]["asset"]
elif platform == "lbtc":
asset = "BTC"
if asset == "XMR":
amount_crypto = contact["data"]["amount_xmr"]
history = self.cg.get_coin_history_by_id(id="monero", date=date_formatted)
crypto_usd = float(history["market_data"]["current_price"]["usd"])
elif asset == "BTC":
amount_crypto = contact["data"]["amount_btc"]
history = self.cg.get_coin_history_by_id(id="bitcoin", date=date_formatted)
crypto_usd = float(history["market_data"]["current_price"]["usd"])
# Convert crypto to fiat
amount = float(amount_crypto) * crypto_usd
currency = contact["data"]["currency"]
if not contact["data"]["is_selling"]:
continue
if currency == "USD":
cumul_usd += float(amount)
else:
rate = rates[currency]
amount_usd = float(amount) / rate
cumul_usd += amount_usd
return cumul_usd
@inlineCallbacks
def get_open_trades_usd(self):
"""
Get total value of open trades in USD.
:return: total trade value
:rtype: float
"""
dash_agora = self.agora.wrap_dashboard()
dash_lbtc = self.lbtc.wrap_dashboard()
dash_agora = yield dash_agora
dash_lbtc = yield dash_lbtc
if dash_agora is False:
return False
if dash_lbtc is False:
return False
rates = self.get_rates_all()
cumul_usd_agora = self.open_trades_usd_parse_dash("agora", dash_agora, rates)
cumul_usd_lbtc = self.open_trades_usd_parse_dash("lbtc", dash_lbtc, rates)
cumul_usd = cumul_usd_agora + cumul_usd_lbtc
cast_es = {
"trades_usd": cumul_usd,
}
self.write_to_es("get_open_trades_usd", cast_es)
return cumul_usd
@inlineCallbacks
def get_total_remaining(self):
"""
Check how much profit we need to make in order to withdraw, taking into account open trade value.
:return: profit remaining in USD
:rtype: float
"""
total_usd = yield self.get_total_usd()
total_trades_usd = yield self.get_open_trades_usd()
if not total_usd:
return False
total_usd += total_trades_usd
withdraw_threshold = float(settings.Money.BaseUSD) + float(settings.Money.WithdrawLimit)
remaining = withdraw_threshold - total_usd
cast_es = {
"total_remaining_usd": remaining,
}
self.write_to_es("get_total_remaining", cast_es)
return remaining
@inlineCallbacks
def get_total_with_trades(self):
total_usd = yield self.get_total_usd()
if not total_usd:
return False
total_trades_usd = yield self.get_open_trades_usd()
total_with_trades = total_usd + total_trades_usd
cast_es = {
"total_with_trades": total_with_trades,
}
self.write_to_es("get_total_with_trades", cast_es)
return total_with_trades