Implement fetching account balances
This commit is contained in:
parent
dc1b11bf1e
commit
d93eb8e936
|
@ -82,3 +82,18 @@ class Account(Model):
|
|||
|
||||
class AccountDetails(Model):
|
||||
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)
|
||||
|
|
|
@ -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()
|
|
@ -98,6 +98,19 @@ class Money(util.Base):
|
|||
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
|
||||
def get_profit(self, trades=False):
|
||||
"""
|
||||
|
|
|
@ -87,3 +87,16 @@ class Sinks(util.Base):
|
|||
# {"EUR": {"IBAN": "xxx", "BIC": "xxx"},
|
||||
# "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}}
|
||||
# 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
|
||||
|
|
|
@ -5,7 +5,7 @@ from twisted.internet.task import LoopingCall
|
|||
import requests
|
||||
from simplejson.errors import JSONDecodeError
|
||||
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
|
||||
|
||||
# Project imports
|
||||
|
@ -242,9 +242,6 @@ class Nordigen(util.Base):
|
|||
continue
|
||||
accounts = self.get_accounts(req["id"])
|
||||
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)
|
||||
if not account_info:
|
||||
continue
|
||||
|
@ -281,7 +278,55 @@ class Nordigen(util.Base):
|
|||
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
|
||||
path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/"
|
||||
r = requests.get(path, headers=headers)
|
||||
try:
|
||||
obj = TXRoot.from_json(r.content)
|
||||
except ValidationError as err:
|
||||
self.log.error(f"Validation error: {err}")
|
||||
return
|
||||
parsed = obj.to_dict()["transactions"]["booked"]
|
||||
self.normalise_transactions(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
|
||||
|
|
|
@ -6,7 +6,9 @@ import requests
|
|||
from simplejson.errors import JSONDecodeError
|
||||
from time import time
|
||||
from json import dumps, loads
|
||||
from lib.serde.truelayer import AccountBalancesRoot
|
||||
import urllib
|
||||
from serde import ValidationError
|
||||
|
||||
# Project imports
|
||||
from settings import settings
|
||||
|
@ -274,8 +276,55 @@ class TrueLayer(util.Base):
|
|||
parsed = r.json()
|
||||
except JSONDecodeError:
|
||||
self.log.error(f"Error parsing transactions response: {r.content}")
|
||||
return False
|
||||
return (False, False)
|
||||
if "results" in parsed:
|
||||
return parsed["results"]
|
||||
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
|
||||
|
|
|
@ -263,8 +263,11 @@ class Transactions(util.Base):
|
|||
matching_refs = []
|
||||
# TODO: use get_ref_map in this function instead of calling get_ref multiple times
|
||||
for ref in refs:
|
||||
print(f"ITER 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):
|
||||
print(f"APPENDING STORED TRADE AS MATCH {stored_trade}")
|
||||
matching_refs.append(stored_trade)
|
||||
if len(matching_refs) != 1:
|
||||
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
|
||||
:rtype float:
|
||||
"""
|
||||
# TODO: get Sink totals
|
||||
total_sinks_usd = self.sinks.get_total_usd()
|
||||
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
|
||||
if not agora_wallet_xmr["success"]:
|
||||
return False
|
||||
|
@ -391,9 +394,7 @@ class Transactions(util.Base):
|
|||
|
||||
# Add it all up
|
||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
||||
# total_usd = total_usd_agora + total_usd_revolut
|
||||
# TODO: add sinks value here
|
||||
total_usd = total_usd_agora
|
||||
total_usd = total_usd_agora + total_sinks_usd
|
||||
cast_es = {
|
||||
"price_usd": total_usd,
|
||||
"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))
|
||||
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
|
||||
"""
|
||||
# TODO: get sinks value here
|
||||
# total_usd_revolut = self.revolut.get_total_usd()
|
||||
total_sinks_usd = self.sinks.get_total_usd()
|
||||
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
|
||||
if not agora_wallet_xmr["success"]:
|
||||
self.log.error("Could not get Agora XMR wallet total.")
|
||||
|
@ -442,9 +442,7 @@ class Transactions(util.Base):
|
|||
|
||||
# Add it all up
|
||||
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
|
||||
# total_usd = total_usd_agora + total_usd_revolut
|
||||
# TODO: add sinks value here
|
||||
total_usd = total_usd_agora
|
||||
total_usd = total_usd_agora + total_sinks_usd
|
||||
|
||||
# Convert the total USD price to GBP and SEK
|
||||
rates = self.money.get_rates_all()
|
||||
|
|
Loading…
Reference in New Issue