Implement fetching account balances
This commit is contained in:
parent
dc1b11bf1e
commit
d93eb8e936
|
@ -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)
|
||||||
|
|
|
@ -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()
|
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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
obj = TXRoot.from_json(r.content)
|
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"]
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue