env
.idea/
test*.py
-
+*.pyc
* Triodos Bank
* BBBank eG
* Postbank
-* [1822direkt](https://www.1822direkt.de/service/zugang-zum-konto/softwarebanking-mit-hbci/)
+* [1822direkt](https://www.1822direkt.de/service/zugang-zum-konto/softwarebanking-mit-hbci/), including access to holding accounts
* Sparkasse
* Ing-Diba
+* CortalConsors, including access to holding accounts
Usage
-----
# https://mt940.readthedocs.io/en/latest/mt940.html#mt940.models.Transaction
# for documentation. Most information is contained in a dict accessible via their
# ``data`` property
+
+# for retrieving the holdings of an account:
+holdings = f.get_holdings()
+# holdings contains a list of namedtuple values containing ISIN, name,
+# market_value, pieces, total_value and valuation_date as parsed from
+# the MT535 message.
```
Credits and License
from .dialog import FinTSDialog
from .message import FinTSMessage
from .models import SEPAAccount
-from .models import Saldo
from .segments.accounts import HKSPA
from .segments.statement import HKKAZ
from .segments.saldo import HKSAL
-from .utils import mt940_to_array
+from .segments.depot import HKWPD
+from .utils import mt940_to_array, MT535_Miniparser
from mt940.models import Balance
logger = logging.getLogger(__name__)
logger.debug('Sending HKSAL: {}'.format(msg))
resp = dialog.send(msg)
logger.debug('Got HKSAL response: {}'.format(resp))
-
+
# end dialog
dialog.end()
# find segment and split up to balance part
seg = resp._find_segment('HISAL')
arr = seg.split('+')[4].split(':')
-
+
# get balance date
date = datetime.datetime.strptime(arr[3], "%Y%m%d").date()
-
+
# return balance
return Balance(arr[0], arr[1], date, currency=arr[2])
)
])
+ def get_holdings(self, account):
+ # init dialog
+ dialog = self._new_dialog()
+ dialog.sync()
+ dialog.init()
+
+ # execute job
+ msg = self._create_get_holdings_message(dialog, account)
+ logger.debug('Sending HKWPD: {}'.format(msg))
+ resp = dialog.send(msg)
+ logger.debug('Got HIWPD response: {}'.format(resp))
+
+ # end dialog
+ dialog.end()
+
+ # find segment and split up to balance part
+ seg = resp._find_segment('HIWPD')
+ if seg:
+ mt535_lines = str.splitlines(seg)
+ # The first line contains a FinTS HIWPD header - drop it.
+ del mt535_lines[0]
+ mt535 = MT535_Miniparser()
+ return mt535.parse(mt535_lines)
+ else:
+ logger.debug('No HIWPD response segment found - maybe account has no holdings?')
+ return []
+
+ def _create_get_holdings_message(self, dialog: FinTSDialog, account: SEPAAccount):
+ hversion = dialog.hksalversion
+
+ if hversion in (1, 2, 3, 4, 5, 6):
+ acc = ':'.join([
+ account.accountnumber, account.subaccount, str(280), account.blz
+ ])
+ elif hversion == 7:
+ acc = ':'.join([
+ account.iban, account.bic, account.accountnumber, account.subaccount, str(280), account.blz
+ ])
+ else:
+ raise ValueError('Unsupported HKSAL version {}'.format(hversion))
+
+ return self._new_message(dialog, [
+ HKWPD(
+ 3,
+ hversion,
+ acc,
+ )
+ ])
+
+
class FinTS3PinTanClient(FinTS3Client):
+
def __init__(self, blz, username, pin, server):
self.username = username
self.blz = blz
SEPAAccount = namedtuple('SEPAAccount', 'iban bic accountnumber subaccount blz')
Saldo = namedtuple('Saldo', 'account date value currency')
+
+Holding = namedtuple('Holding', 'ISIN name market_value value_symbol valuation_date pieces total_value')
--- /dev/null
+from . import FinTS3Segment
+
+
+class HKWPD(FinTS3Segment):
+ """
+ HKWPD (Depotaufstellung anfordern)
+ Section C.4.3.1
+ Example: HKWPD:3:7+23456::280:10020030+USD+2'
+ """
+ type = 'HKWPD'
+
+ def __init__(self, segno, version, account):
+ self.version = version
+ data = [
+ account
+ # The spec allows the specification of currency and
+ # quality of valuations, as shown in the docstring above.
+ # However, both 1822direkt and CortalConsors reject the
+ # call if these two are present, claiming an invalid input.
+ # 'EUR' # Währung der Depotaufstellung"
+ # 2 # Kursqualität
+ ]
+ super().__init__(segno, data)
from . import FinTS3Segment
+
class HKSAL(FinTS3Segment):
"""
HKSAL (Konto Saldo anfordern)
import mt940
+import re
+from .models import Holding
+from datetime import datetime
def mt940_to_array(data):
data = data.replace("-0000", "+0000")
transactions = mt940.models.Transactions()
return transactions.parse(data)
+
+
+def print_segments(message):
+ segments = str(message).split("'")
+ for idx, seg in enumerate(segments):
+ print(u"{}: {}".format(idx, seg.encode('utf-8')))
+
+
+class MT535_Miniparser:
+ re_identification = re.compile(r"^:35B:ISIN\s(.*)\|(.*)\|(.*)$")
+ re_marketprice = re.compile(r"^:90B::MRKT\/\/ACTU\/([A-Z]{3})(\d*),{1}(\d*)$")
+ re_pricedate = re.compile(r"^:98A::PRIC\/\/(\d*)$")
+ re_pieces = re.compile(r"^:93B::AGGR\/\/UNIT\/(\d*),(\d*)$")
+ re_totalvalue = re.compile(r"^:19A::HOLD\/\/([A-Z]{3})(\d*),{1}(\d*)$")
+
+ def parse(self, lines):
+ retval = []
+ # First: Collapse multiline clauses into one clause
+ clauses = self.collapse_multilines(lines)
+ # Second: Scan sequence of clauses for financial instrument
+ # sections
+ finsegs = self.grab_financial_instrument_segments(clauses)
+ # Third: Extract financial instrument data
+ for finseg in finsegs:
+ isin, name, market_price, price_symbol, price_date, pieces = (None,)*6
+ for clause in finseg:
+ # identification of instrument
+ # e.g. ':35B:ISIN LU0635178014|/DE/ETF127|COMS.-MSCI EM.M.T.U.ETF I'
+ m = self.re_identification.match(clause)
+ if m:
+ isin = m.group(1)
+ name = m.group(3)
+ # current market price
+ # e.g. ':90B::MRKT//ACTU/EUR38,82'
+ m = self.re_marketprice.match(clause)
+ if m:
+ price_symbol = m.group(1)
+ market_price = float(m.group(2) + "." + m.group(3))
+ # date of market price
+ # e.g. ':98A::PRIC//20170428'
+ m = self.re_pricedate.match(clause)
+ if m:
+ price_date = datetime.strptime(m.group(1), "%Y%m%d").date()
+ # number of pieces
+ # e.g. ':93B::AGGR//UNIT/16,8211'
+ m = self.re_pieces.match(clause)
+ if m:
+ pieces = float(m.group(1) + "." + m.group(2))
+ # total value of holding
+ # e.g. ':19A::HOLD//EUR970,17'
+ m = self.re_totalvalue.match(clause)
+ if m:
+ total_value = float(m.group(2) + "." + m.group(3))
+ # processed all clauses
+ retval.append(
+ Holding(
+ ISIN=isin, name=name, market_value=market_price,
+ value_symbol=price_symbol, valuation_date=price_date,
+ pieces=pieces, total_value=total_value))
+ return retval
+
+ def collapse_multilines(self, lines):
+ clauses = []
+ prevline = ""
+ for line in lines:
+ if line.startswith(":"):
+ if prevline != "":
+ clauses.append(prevline)
+ prevline = line
+ elif line.startswith("-"):
+ # last line
+ clauses.append(prevline)
+ clauses.append(line)
+ else:
+ prevline += "|{}".format(line)
+ return clauses
+
+ def grab_financial_instrument_segments(self, clauses):
+ retval = []
+ stack = []
+ within_financial_instrument = False
+ for clause in clauses:
+ if clause.startswith(":16R:FIN"):
+ # start of financial instrument
+ within_financial_instrument = True
+ elif clause.startswith(":16S:FIN"):
+ # end of financial instrument - move stack over to
+ # return value
+ retval.append(stack)
+ stack = []
+ within_financial_instrument = False
+ else:
+ if within_financial_instrument:
+ stack.append(clause)
+ return retval