Implement fetching account balances

This commit is contained in:
Mark Veidemanis 2022-04-09 19:59:22 +01:00
parent dc1b11bf1e
commit d93eb8e936
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 163 additions and 16 deletions

View File

@ -82,3 +82,18 @@ class Account(Model):
class AccountDetails(Model): class AccountDetails(Model):
account: fields.Nested(Account) account: fields.Nested(Account)
class AccountBalanceAmount(Model):
amount: fields.Str()
currency: fields.Str()
class AccountBalances(Model):
balanceAmount: fields.Nested(AccountBalanceAmount)
balanceType: fields.Str()
referenceDate: fields.Date()
class AccountBalancesRoot(Model):
balances = fields.List(AccountBalances)

View File

@ -0,0 +1,14 @@
from serde import Model, fields
class AccountBalances(Model):
currency: fields.Str()
available: fields.Float()
current: fields.Float()
overdraft: fields.Float()
update_timestamp: fields.DateTime()
class AccountBalancesRoot(Model):
results: fields.List(AccountBalances)
status: fields.Str()

View File

@ -98,6 +98,19 @@ class Money(util.Base):
rates = self.get_rates_all() rates = self.get_rates_all()
return float(amount) / rates[currency] 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 # TODO: move to money
def get_profit(self, trades=False): def get_profit(self, trades=False):
""" """

View File

@ -87,3 +87,16 @@ class Sinks(util.Base):
# {"EUR": {"IBAN": "xxx", "BIC": "xxx"}, # {"EUR": {"IBAN": "xxx", "BIC": "xxx"},
# "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}} # "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}}
# self.markets.distribute_account_details(currencies, account_infos) # self.markets.distribute_account_details(currencies, account_infos)
def get_total_usd(self):
"""
Get the total balance of our accounts in USD.
"""
total_nordigen = self.nordigen.get_total_map()
total_truelayer = self.truelayer.get_total_map()
# Yes, we can save an API call by merging but I think this is clearer
total_nordigen_usd = self.money.multiple_to_usd(total_nordigen)
total_truelayer_usd = self.money.multiple_to_usd(total_truelayer)
return total_truelayer_usd + total_nordigen_usd

View File

@ -5,7 +5,7 @@ from twisted.internet.task import LoopingCall
import requests import requests
from simplejson.errors import JSONDecodeError from simplejson.errors import JSONDecodeError
from json import dumps, loads from json import dumps, loads
from lib.serde.nordigen import TXRoot, AccessToken, Institutions, Agreement, Requisitions, AccountDetails from lib.serde.nordigen import TXRoot, AccessToken, Institutions, Agreement, Requisitions, AccountDetails, AccountBalancesRoot
from serde import ValidationError from serde import ValidationError
# Project imports # Project imports
@ -242,9 +242,6 @@ class Nordigen(util.Base):
continue continue
accounts = self.get_accounts(req["id"]) accounts = self.get_accounts(req["id"])
for account_id in accounts: for account_id in accounts:
# if not account_id in self.banks:
# print("account_id", account_id, "not in self.banks!")
# continue
account_info = self.get_account(account_id) account_info = self.get_account(account_id)
if not account_info: if not account_info:
continue continue
@ -281,7 +278,55 @@ class Nordigen(util.Base):
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"} headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/" path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/"
r = requests.get(path, headers=headers) r = requests.get(path, headers=headers)
try:
obj = TXRoot.from_json(r.content) obj = TXRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["transactions"]["booked"] parsed = obj.to_dict()["transactions"]["booked"]
self.normalise_transactions(parsed) self.normalise_transactions(parsed)
return parsed return parsed
def get_balance(self, account_id):
"""
Get the balance and currency of an account.
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/balances/"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["balances"]
total = 0
currency = None
for entry in parsed:
if currency:
if not currency == entry["balanceAmount"]["currency"]:
self.log.error("Different currencies in balance query.")
return
total += float(entry["balanceAmount"]["amount"])
currency = entry["balanceAmount"]["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for account_id in self.banks:
currency, amount = self.get_balance(account_id)
if not amount:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

View File

@ -6,7 +6,9 @@ import requests
from simplejson.errors import JSONDecodeError from simplejson.errors import JSONDecodeError
from time import time from time import time
from json import dumps, loads from json import dumps, loads
from lib.serde.truelayer import AccountBalancesRoot
import urllib import urllib
from serde import ValidationError
# Project imports # Project imports
from settings import settings from settings import settings
@ -274,8 +276,55 @@ class TrueLayer(util.Base):
parsed = r.json() parsed = r.json()
except JSONDecodeError: except JSONDecodeError:
self.log.error(f"Error parsing transactions response: {r.content}") self.log.error(f"Error parsing transactions response: {r.content}")
return False return (False, False)
if "results" in parsed: if "results" in parsed:
return parsed["results"] return parsed["results"]
else: else:
return False return (False, False)
def get_balance(self, bank, account_id):
"""
Get the balance of an account.
:param bank: the bank to check
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/balance"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["results"]
total = 0
currency = None
for entry in parsed:
if currency:
if not currency == entry["currency"]:
self.log.error("Different currencies in balance query.")
return
total += entry["available"]
currency = entry["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for bank in self.banks:
for account_id in self.banks[bank]:
currency, amount = self.get_balance(bank, account_id)
if not amount:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

View File

@ -263,8 +263,11 @@ class Transactions(util.Base):
matching_refs = [] matching_refs = []
# TODO: use get_ref_map in this function instead of calling get_ref multiple times # TODO: use get_ref_map in this function instead of calling get_ref multiple times
for ref in refs: for ref in refs:
print(f"ITER REF {ref}")
stored_trade = self.get_ref(ref) stored_trade = self.get_ref(ref)
print(f"ITER REF STORED TRADE {stored_trade}")
if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount): if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount):
print(f"APPENDING STORED TRADE AS MATCH {stored_trade}")
matching_refs.append(stored_trade) matching_refs.append(stored_trade)
if len(matching_refs) != 1: if len(matching_refs) != 1:
self.log.error(f"Find trade returned multiple results for TXID {txid}: {matching_refs}") self.log.error(f"Find trade returned multiple results for TXID {txid}: {matching_refs}")
@ -368,7 +371,7 @@ class Transactions(util.Base):
:return: value in USD :return: value in USD
:rtype float: :rtype float:
""" """
# TODO: get Sink totals total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr() agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]: if not agora_wallet_xmr["success"]:
return False return False
@ -391,9 +394,7 @@ class Transactions(util.Base):
# Add it all up # Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
# total_usd = total_usd_agora + total_usd_revolut total_usd = total_usd_agora + total_sinks_usd
# TODO: add sinks value here
total_usd = total_usd_agora
cast_es = { cast_es = {
"price_usd": total_usd, "price_usd": total_usd,
"total_usd_agora_xmr": total_usd_agora_xmr, "total_usd_agora_xmr": total_usd_agora_xmr,
@ -416,8 +417,7 @@ class Transactions(util.Base):
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC)) :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)) :rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
""" """
# TODO: get sinks value here total_sinks_usd = self.sinks.get_total_usd()
# total_usd_revolut = self.revolut.get_total_usd()
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr() agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]: if not agora_wallet_xmr["success"]:
self.log.error("Could not get Agora XMR wallet total.") self.log.error("Could not get Agora XMR wallet total.")
@ -442,9 +442,7 @@ class Transactions(util.Base):
# Add it all up # Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
# total_usd = total_usd_agora + total_usd_revolut total_usd = total_usd_agora + total_sinks_usd
# TODO: add sinks value here
total_usd = total_usd_agora
# Convert the total USD price to GBP and SEK # Convert the total USD price to GBP and SEK
rates = self.money.get_rates_all() rates = self.money.get_rates_all()