Skip to content

MT535: reset total_value for each holding - #231

Open
mankind806 wants to merge 1 commit into
raphaelm:masterfrom
mankind806:fix-mt535-total-value
Open

mankind806 wants to merge 1 commit into
raphaelm:masterfrom
mankind806:fix-mt535-total-value

Conversation

@mankind806

Copy link
Copy Markdown

Describe the bug

MT535_Miniparser.parse does not reset total_value for each holding. A holding without a :19A::HOLD// clause gets the total value of the holding before it in the same message. If the first holding in the message has no :19A: clause, parse raises UnboundLocalError, so get_holdings() fails.

Cause

fints/utils.py at v5.0.1 (and at master eda20b4):

  • line 165: isin, name, market_price, price_symbol, price_date, pieces, acquisitionprice = (None,)*7 resets seven of the eight values of a holding. total_value is not among them.
  • lines 191-193: total_value is assigned only if a :19A::HOLD//... clause matches.
  • line 205: Holding(..., total_value=total_value, ...) always reads it. For the second and later holdings that is the value from the previous loop pass; for the first holding it is a name that was never assigned.

get_holdings (fints/client.py:704-714) creates one MT535_Miniparser per HIWPD segment and calls parse on its lines, so the carry-over runs across all holdings of one segment.

Impact

  • A holding without :19A: is reported with a plausible but wrong total value (the one of its neighbour) and no warning. A caller that sums the depot value or stores it gets a wrong number.
  • If the first holding has no :19A:, get_holdings() raises UnboundLocalError: cannot access local variable 'total_value' and no holding is returned.

I did not show that a particular bank omits :19A:. The parser treats the clause as optional (it only matches it if present), so the missing case should be handled either way.

Affected versions

python-fints 5.0.1 (tag v5.0.1, commit eda20b4fec2e612dc7942234884be2e8c4fe632f) and master at the same commit (checked 2026-11-09; fints/utils.py is identical in both). Older versions were not tested.

Reproduction

No network, synthetic data. Python 3.14.8, python-fints 5.0.1 from PyPI.

from fints.utils import MT535_Miniparser


def fin(isin, name, pieces, value=None):
    lines = [":16R:FIN", f":35B:ISIN {isin}|/DE/X1|{name}",
             ":90B::MRKT//ACTU/EUR10,00", ":98A::PRIC//20260101",
             f":93B::AGGR//UNIT/{pieces},"]
    if value is not None:
        lines.append(f":19A::HOLD//EUR{value}")
    lines.append(":16S:FIN")
    return lines


def parse(*holdings):
    lines = [l for h in holdings for l in h] + ["-"]  # an MT535 ends with a "-" line
    return MT535_Miniparser().parse(lines)


# A: the second holding has no :19A:
for h in parse(fin("AA0000000001", "ONE", "5", "50,00"), fin("AA0000000002", "TWO", "7")):
    print("A", h.name, h.pieces, h.total_value)

# B: the first holding has no :19A:
try:
    parse(fin("AA0000000003", "THREE", "1"))
except Exception as e:
    print("B", type(e).__name__, e)

Actual output:

A ONE 5.0 50.0
A TWO 7.0 50.0
B UnboundLocalError cannot access local variable 'total_value' where it is not associated with a value

Expected: A TWO 7.0 None, and for B a holding with total_value=None.

Not checked

  • No real bank and no real MT535 message; whether banks omit :19A: for some holdings (or in which cases) is not known to me. The SWIFT/DK message definition was not read for this report.
  • Only total_value was examined; the other seven values are reset in line 165.
  • Python versions other than 3.14.8, and versions before 5.0.1.
  • The full test suite was not run (pytest was not available in my environment); the two new tests were run as plain function calls.

Solution

One line (fints/utils.py:165) and two tests (new file tests/test_mt535.py). The patch is against eda20b4 (= v5.0.1 = master on 2026-11-09). The new tests fail before the change (case B with UnboundLocalError, case A with the wrong value 50.0) and pass after it.

diff --git a/fints/utils.py b/fints/utils.py
index f02edb6..a656131 100644
--- a/fints/utils.py
+++ b/fints/utils.py
@@ -162,7 +162,7 @@ class MT535_Miniparser:
         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, acquisitionprice = (None,)*7
+            isin, name, market_price, price_symbol, price_date, pieces, total_value, acquisitionprice = (None,)*8
             for clause in finseg:
                 # identification of instrument
                 # e.g. ':35B:ISIN LU0635178014|/DE/ETF127|COMS.-MSCI EM.M.T.U.ETF I'
diff --git a/tests/test_mt535.py b/tests/test_mt535.py
new file mode 100644
index 0000000..69b3b49
--- /dev/null
+++ b/tests/test_mt535.py
@@ -0,0 +1,34 @@
+from fints.utils import MT535_Miniparser
+
+
+def _mt535_holding(isin, name, pieces, total_value=None):
+    lines = [
+        ":16R:FIN",
+        ":35B:ISIN {}|/DE/X1|{}".format(isin, name),
+        ":90B::MRKT//ACTU/EUR10,00",
+        ":98A::PRIC//20260101",
+        ":93B::AGGR//UNIT/{},".format(pieces),
+    ]
+    if total_value is not None:
+        lines.append(":19A::HOLD//EUR{}".format(total_value))
+    lines.append(":16S:FIN")
+    return lines
+
+
+def test_mt535_holding_without_total_value_does_not_inherit_previous():
+    lines = _mt535_holding("AA0000000001", "ONE", "5", "50,00")
+    lines += _mt535_holding("AA0000000002", "TWO", "7")
+    lines.append("-")
+
+    first, second = MT535_Miniparser().parse(lines)
+
+    assert first.total_value == 50.0
+    assert second.total_value is None
+
+
+def test_mt535_first_holding_without_total_value():
+    lines = _mt535_holding("AA0000000003", "THREE", "1") + ["-"]
+
+    (holding,) = MT535_Miniparser().parse(lines)
+
+    assert holding.total_value is None

AI disclosure

An AI assistant (Claude, Anthropic) helped find and write this report and the patch. The reproduction and the two tests were run on python-fints 5.0.1, and the cause was checked against the source by a second review step. The account owner allows AI-prepared bug-fix submissions; questions are welcome and will be answered.

A holding without a :19A::HOLD// clause got the total value of the
previous holding; if the first holding had none, parse() raised
UnboundLocalError. Reset total_value with the other per-holding values
and add two tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWM3zdiEV9ZBtiNXUKHuqw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant