Trading  ·  BraivIQ AI Engineering Playbook

Trading Workflow Architecture For The FCA's 2028 Transaction Reports: Build, Check And Approve The Draft Schema In Python

A trading workflow architecture for the FCA's new transaction reports has three stages before anything is sent: build the auth.016 XML from your trade records, check it against the draft schema and content rules the FCA published on 2 October 2026, and have a named person approve the exact file. This playbook runs real Python against that draft XSD and the V1.3 draft validation rules. It shows the timestamp format that fails the schema, the package fields that are new, two places where the draft documents disagree, and a release step that refuses any file edited after approval.

Published  ·  Updated  ·  15 min read  ·  By BraivIQ Engineering

Market data screens with index prices and charts, illustrating trading workflow architecture for FCA transaction reports at a UK firm

Key takeaways

  • The FCA published a draft transaction reporting schema and V1.3 draft validation rules on 2 October 2026 with CP26/34. Comments close on 6 November 2026, and the new format replaces the UK MiFIR formats on 3 April 2028.
  • The draft XSD requires trading date times ending in Z. Python's datetime.isoformat() writes +00:00, which fails, so format timestamps explicitly.
  • New fields include a client indicator for buyer and seller, a package identifier and a package transaction price. The schema leaves the client indicator optional, but draft rule CON-080 makes it mandatory from 3 April 2028.
  • Schema errors reject the whole file, while content rules reject individual transactions, so run both before a person approves anything.
  • Bind the approval to the file's SHA-256 and refuse to release a file whose bytes changed afterwards.

52 - Fields in the new UK transaction report, down from 65 (PS26/15)  ·  3 Apr 2028 - Date FCA systems switch to the new schema and stop accepting UK MiFIR formats (CP26/34)  ·  6 Nov 2026 - Deadline for comments on CP26/34 and the draft schema  ·  £100m+ - Annual saving in reporting costs the FCA expects from the changes (PS26/15)

Treat the new report as a pipeline with a human gate, not a file export. A good trading workflow architecture builds the XML from trade records with deterministic code, validates it against the published schema and the content rules, sends exceptions back to the desk with the rule and field, and lets a named person approve the exact file before release. Software makes the file and checks it. A person decides it can go.

Everything below runs against the files the FCA published on 2 October 2026. The samples use made-up LEIs, references and prices, pass mypy 2.4.0 in strict mode on Python 3.14.4 and 3.11.14, and use xmlschema 4.3.2. Nothing here submits anything to the FCA.

What did the FCA publish on 2 October 2026?

Consultation paper CP26/34 sets out how firms move to the new regime agreed in policy statement PS26/15. Alongside it the FCA posted a draft schema, labelled “MiFID II - Transaction Reporting - DRAFT - V1.0.0”, and a draft validation rules workbook, version 1.3. The paper says that on 3 April 2028 the FCA's systems will switch over and firms will no longer be able to submit transaction reports in the UK MiFIR formats.

For engineers, five details matter. The schema's target namespace is urn:fca:org:uk:tech:xsd:auth.016.200.03, although the file name says 001.03. The trading date time must match the pattern .*Z. Package trades get a package identifier, field 37, limited to 35 capital letters and digits, and a package transaction price, field 38. Buyer and seller each get a client indicator. Several elements of today's schema, including the short selling and waiver indicators, are gone.

What does a trading workflow architecture for the new reports look like?

Trade and order records feed a report builder. The file goes through the schema check, where any error rejects everything, then through content rules that work per transaction. Failures go back to the desk with the rule ID and field. Clean files go to a named reviewer, who approves a hash of the file. The release step refuses anything unapproved or edited, and every approval, release and refusal goes into a hash-chained audit log.

Trading workflow architecture diagram: trade records feed a report builder that writes auth.016 XML, a schema check against the FCA draft XSD, content rules such as CON-080, CON-380 and CON-400, exceptions back to the desk, a named reviewer who approves the file hash, a release gate that refuses unapproved or edited files, submission of the approved bytes and a hash-chained audit log.
Code builds and checks. A person approves the file. Tap to open full size.

How do you build an auth.016 report in Python?

The builder uses only the standard library. It writes a two-leg spread under package SPRD00001, the example the FCA uses in Appendix 2 of CP26/34, plus one client trade in an agency capacity. Two helpers carry most of the risk. utc_z refuses a timestamp without a time zone and writes the Z suffix. The amount helper writes negative prices as a positive amount with a sign flag, because the amount element in the schema cannot be negative.

build_report.py
"""Build a transaction report in the FCA's draft 2028 format (auth.016, DATTRA draft V1.0.0).

Sample data only: the LEIs, trade references and prices below are made up.
Standard library only. Tested on Python 3.14.4 and 3.11.14 with mypy --strict.
"""

from __future__ import annotations

import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path

# Target namespace of auth.016.001.03_FCA_DATTRA_1.0.0.xsd (published 2 October 2026).
NS = "urn:fca:org:uk:tech:xsd:auth.016.200.03"
PACKAGE_ID = re.compile(r"[A-Z0-9]{1,35}")  # Field 37 pattern in the draft XSD


@dataclass(frozen=True)
class Party:
    lei: str
    is_client: bool  # Field 8 / 17, "Client indicator", mandatory from 3 April 2028


@dataclass(frozen=True)
class Trade:
    tx_id: str
    executing_lei: str
    buyer: Party
    seller: Party
    traded_at: datetime  # must carry a time zone
    capacity: str  # DEAL, MTCH or AOTC
    quantity: Decimal
    price: Decimal
    currency: str
    venue_mic: str
    isin: str
    algo_id: str
    package_id: str | None = None
    package_price: Decimal | None = None


def utc_z(ts: datetime) -> str:
    """YYYY-MM-DDThh:mm:ss.ssssssZ. datetime.isoformat() gives +00:00, which the draft XSD rejects."""
    if ts.tzinfo is None:
        raise ValueError("naive datetime: refusing to guess the time zone")
    return ts.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")


def money(value: Decimal) -> str:
    return format(value, "f")


def _el(parent: ET.Element, tag: str, text: str | None = None, **attrs: str) -> ET.Element:
    el = ET.SubElement(parent, f"{{{NS}}}{tag}", attrs)
    if text is not None:
        el.text = text
    return el


def _party(parent: ET.Element, tag: str, party: Party) -> None:
    node = _el(parent, tag)
    _el(node, "ClntInd", "true" if party.is_client else "false")
    _el(_el(_el(node, "AcctOwnr"), "Id"), "LEI", party.lei)


def _amount(parent: ET.Element, value: Decimal, currency: str) -> None:
    """AmountAndDirection61: Amt has minInclusive 0, so a negative price is Amt plus Sgn=false."""
    mntry = _el(_el(parent, "Pric"), "MntryVal")
    _el(mntry, "Amt", money(abs(value)), Ccy=currency)
    if value < 0:
        _el(mntry, "Sgn", "false")


def add_new_report(doc_root: ET.Element, trade: Trade) -> None:
    if trade.capacity not in {"DEAL", "MTCH", "AOTC"}:
        raise ValueError(f"{trade.tx_id}: unknown trading capacity {trade.capacity}")
    if trade.package_id is not None and not PACKAGE_ID.fullmatch(trade.package_id):
        raise ValueError(f"{trade.tx_id}: package id must be 1-35 capital letters or digits")
    new = _el(_el(doc_root, "Tx"), "New")
    _el(new, "TxId", trade.tx_id)
    _el(new, "ExctgPty", trade.executing_lei)
    _el(new, "ExctgNttyTxRptngPtyInd", "true")
    _el(new, "SubmitgPty", trade.executing_lei)
    _party(new, "Buyr", trade.buyer)
    _party(new, "Sellr", trade.seller)
    _el(new, "OrdrTrnsmssn")  # mandatory element, both children optional
    tx = _el(new, "Tx")
    _el(tx, "TradDt", utc_z(trade.traded_at))
    _el(tx, "TradgCpcty", trade.capacity)
    _el(_el(tx, "Qty"), "Unit", money(trade.quantity))
    _amount(_el(tx, "Pric"), trade.price, trade.currency)
    _el(tx, "TradVn", trade.venue_mic)
    if trade.package_id is not None:
        _el(tx, "PckgId", trade.package_id)
        if trade.package_price is not None:
            _amount(_el(tx, "PckgTxPric"), trade.package_price, trade.currency)
    _el(_el(new, "FinInstrm"), "Id", trade.isin)
    _el(_el(new, "InvstmtDcsnPrsn"), "Algo", trade.algo_id)
    _el(_el(new, "ExctgPrsn"), "Algo", trade.algo_id)


def build_document(trades: list[Trade]) -> ET.ElementTree[ET.Element]:
    ET.register_namespace("", NS)
    document = ET.Element(f"{{{NS}}}Document")
    report = _el(document, "FinInstrmRptgTxRpt")
    for trade in trades:
        add_new_report(report, trade)
    tree: ET.ElementTree[ET.Element] = ET.ElementTree(document)
    ET.indent(tree)
    return tree


FIRM = "213800SAMPLEFIRM0001"  # sample LEI pattern, not a real entity
CLIENT = Party("5493000SAMPLECLNT042", is_client=True)
STREET = Party("549300SAMPLECOUNTE07", is_client=False)
WHEN = datetime(2028, 4, 3, 9, 30, 45, 124000, tzinfo=timezone.utc)


def spread_leg(tx_id: str, buyer: Party, seller: Party, price: Decimal, isin: str) -> Trade:
    return Trade(tx_id=tx_id, executing_lei=FIRM, buyer=buyer, seller=seller, traded_at=WHEN,
                 capacity="DEAL", quantity=Decimal("10"), price=price, currency="GBP",
                 venue_mic="XLON", isin=isin, algo_id="EXECALGO1",
                 package_id="SPRD00001", package_price=Decimal("-150.00"))


def sample_trades() -> list[Trade]:
    """A two-leg spread (package SPRD00001, as in CP26/34 Appendix 2) and one client trade."""
    us = Party(FIRM, is_client=False)
    return [
        spread_leg("SPRD00001LEG1", STREET, us, Decimal("3600"), "GB00B03MLX29"),
        spread_leg("SPRD00001LEG2", us, STREET, Decimal("3750"), "GB0002634946"),
        Trade(tx_id="AGENCY000042", executing_lei=FIRM, buyer=CLIENT, seller=STREET, traded_at=WHEN,
              capacity="AOTC", quantity=Decimal("2500"), price=Decimal("12.345"), currency="GBP",
              venue_mic="XLON", isin="GB0007980591", algo_id="EXECALGO1"),
    ]


if __name__ == "__main__":
    out = Path("report.xml")
    build_document(sample_trades()).write(out, encoding="UTF-8", xml_declaration=True)
    print(f"wrote {out} with {len(sample_trades())} new transaction reports")

Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict. Standard library only. Sample LEIs and prices.

How do you check the file before anyone approves it?

The checker runs the draft XSD through xmlschema, then a subset of the draft content rules: future trade dates (CON-270), missing client indicators (CON-080 and CON-170), capacity against the executing entity (CON-280 and CON-281), a missing package price (CON-380), special currency codes (CON-391) and the ISIN check digit (CON-400). It also flags a package whose legs report different package prices, which Appendix 2 asks firms to avoid.

validate_report.py
"""Pre-submission checks for draft 2028 transaction reports.

Stage 1 validates against the FCA's draft XSD (auth.016.001.03_FCA_DATTRA_1.0.0.xsd, 2 October 2026).
Stage 2 applies a subset of the draft content rules (validation rules V1.3 DRAFT, 2 October 2026).
Stage 3 needs a named person to approve the exact file before it can be released, and every step is
written to a hash-chained audit log. Nothing here submits to the FCA.

Requires xmlschema 4.3.2. Tested on Python 3.14.4 and 3.11.14 with mypy --strict. Sample data only.
"""

from __future__ import annotations

import hashlib
import json
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import date, datetime, timezone
from pathlib import Path

import xmlschema

NS = {"t": "urn:fca:org:uk:tech:xsd:auth.016.200.03"}
REGIME_START = date(2028, 4, 3)
# Rule TBC / CON-391: special currency codes not allowed for the package transaction currency
SPECIAL_CCY = {"XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XDR", "XEU", "XFU", "XPD", "XPT", "XXX"}


@dataclass(frozen=True)
class Finding:
    tx_id: str
    code: str  # FCA error code, or GUIDANCE for CP26/34 Appendix 2
    message: str


@dataclass(frozen=True)
class Review:
    file: str
    sha256: str
    reports: int
    schema_errors: list[str]
    findings: list[Finding]

    @property
    def clean(self) -> bool:
        return not self.schema_errors and not self.findings


def sha256_of(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def isin_ok(isin: str) -> bool:
    """ISO 6166 check digit (rule 130, CON-400): letters to numbers, then Luhn from the right."""
    if len(isin) != 12 or not isin[:2].isalpha() or not isin[-1].isdigit():
        return False
    digits = "".join(str(int(ch, 36)) for ch in isin[:11])
    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2 == 0:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return (10 - total % 10) % 10 == int(isin[-1])


def _text(node: ET.Element, path: str) -> str | None:
    found = node.find(path, NS)
    return found.text if found is not None else None


def schema_errors(xsd: Path, report: Path) -> list[str]:
    schema = xmlschema.XMLSchema(str(xsd))
    return [f"{err.path}: {err.reason}" for err in schema.iter_errors(str(report))]


def content_findings(report: Path, as_of: datetime) -> list[Finding]:
    root = ET.parse(report).getroot()
    findings: list[Finding] = []
    package_prices: dict[str, set[str]] = {}
    for new in root.iterfind("t:FinInstrmRptgTxRpt/t:Tx/t:New", NS):
        tx_id = _text(new, "t:TxId") or "(missing TxId)"
        executing = _text(new, "t:ExctgPty")
        traded_raw = _text(new, "t:Tx/t:TradDt") or ""
        try:
            traded = datetime.fromisoformat(traded_raw)
        except ValueError:
            findings.append(Finding(tx_id, "SCHEMA", f"unreadable trading date time {traded_raw!r}"))
            continue
        in_regime = traded.date() >= REGIME_START

        def add(code: str, message: str) -> None:
            findings.append(Finding(tx_id, code, message))

        if traded > as_of:
            add("CON-270", "Trading date time is in the future")
        if in_regime and new.find("t:Buyr/t:ClntInd", NS) is None:
            add("CON-080", "Client indicator for the buyer is missing")
        if in_regime and new.find("t:Sellr/t:ClntInd", NS) is None:
            add("CON-170", "Client indicator for the seller is missing")
        sides = {_text(new, "t:Buyr/t:AcctOwnr/t:Id/t:LEI"), _text(new, "t:Sellr/t:AcctOwnr/t:Id/t:LEI")}
        capacity = _text(new, "t:Tx/t:TradgCpcty")
        if capacity == "DEAL" and executing not in sides:
            add("CON-280", "When using 'DEAL' either Buyer or Seller should be identical with the "
                           "executing entity identification code")
        if capacity in {"MTCH", "AOTC"} and executing in sides:
            add("CON-281", "When using 'MTCH' or 'AOTC' neither Buyer or Seller should be identical "
                           "with the executing entity identification code")
        package = _text(new, "t:Tx/t:PckgId")
        price_node = new.find("t:Tx/t:PckgTxPric", NS)
        if package is not None and price_node is None and in_regime:
            add("CON-380", "The package transaction price is missing")
        if package is not None and price_node is not None:
            amt = price_node.find(".//t:Amt", NS)
            if amt is not None and amt.get("Ccy") in SPECIAL_CCY:
                add("CON-391", "Currency code is not valid for the trade date")
            signature = ET.tostring(price_node, encoding="unicode")
            package_prices.setdefault(package, set()).add(signature)
        isin = _text(new, "t:FinInstrm/t:Id")
        if isin is not None and not isin_ok(isin):
            add("CON-400", "The ISIN code is invalid")
    for package, prices in package_prices.items():
        if len(prices) > 1:
            findings.append(Finding(package, "GUIDANCE",
                                    "Field 38 differs between legs of the same package (CP26/34 Appendix 2)"))
    return findings


def review(xsd: Path, report: Path, as_of: datetime) -> Review:
    root = ET.parse(report).getroot()
    count = len(root.findall("t:FinInstrmRptgTxRpt/t:Tx/t:New", NS))
    return Review(str(report), sha256_of(report), count, schema_errors(xsd, report),
                  content_findings(report, as_of))


def append_audit(log: Path, event: dict[str, object]) -> str:
    """Append-only JSONL. Each entry carries the hash of the previous one, so edits are detectable."""
    prev = "0" * 64
    if log.exists() and log.stat().st_size:
        prev = str(json.loads(log.read_text().splitlines()[-1])["entry_hash"])
    body = {**event, "prev_hash": prev, "at": datetime.now(timezone.utc).isoformat()}
    entry_hash = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()
    with log.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps({**body, "entry_hash": entry_hash}) + "\n")
    return entry_hash


def approve(result: Review, approver: str, log: Path) -> None:
    """A named person approves this exact file. Files with errors cannot be approved."""
    if not result.clean:
        raise PermissionError(f"{result.file}: fix {len(result.schema_errors)} schema errors and "
                              f"{len(result.findings)} findings before approval")
    append_audit(log, {"event": "approved", "file": result.file, "sha256": result.sha256,
                       "reports": result.reports, "approver": approver})


def release_for_submission(report: Path, log: Path) -> Path:
    """Hand the file to your submission gateway only if this exact content was approved."""
    digest = sha256_of(report)
    events = [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
    if not any(e["event"] == "approved" and e["sha256"] == digest for e in events):
        append_audit(log, {"event": "release_refused", "file": str(report), "sha256": digest})
        raise PermissionError(f"{report}: no approval recorded for sha256 {digest[:12]}")
    append_audit(log, {"event": "released", "file": str(report), "sha256": digest})
    return report


def print_review(result: Review) -> None:
    print(f"{result.file}: {result.reports} reports, sha256 {result.sha256[:12]}")
    for err in result.schema_errors:
        print(f"  SCHEMA  {err}")
    for f in result.findings:
        print(f"  {f.code:8} {f.tx_id}: {f.message}")
    if result.schema_errors:
        print("  -> the FCA rejects the whole file on a schema error")
    print("  -> clean" if result.clean else "  -> not ready for approval")


if __name__ == "__main__":
    from build_report import build_document, sample_trades

    xsd = Path(sys.argv[1] if len(sys.argv) > 1 else "fca_draft/auth.016.001.03_FCA_DATTRA_1.0.0.xsd")
    as_of = datetime(2028, 4, 4, 7, 0, tzinfo=timezone.utc)  # simulate a run after the switch-over
    log = Path("audit.jsonl")
    log.unlink(missing_ok=True)

    good = Path("report.xml")
    build_document(sample_trades()).write(good, encoding="UTF-8", xml_declaration=True)
    clean = review(xsd, good, as_of)
    print_review(clean)

    # A copy with three mistakes that are easy to make when moving to the new format
    tree = ET.parse(good)
    news = tree.getroot().findall("t:FinInstrmRptgTxRpt/t:Tx/t:New", NS)
    when = news[0].find("t:Tx/t:TradDt", NS)
    assert when is not None and when.text is not None
    when.text = datetime.fromisoformat(when.text).isoformat()  # +00:00 instead of Z
    tx2 = news[1].find("t:Tx", NS)
    pkg_price = news[1].find("t:Tx/t:PckgTxPric", NS)
    assert tx2 is not None and pkg_price is not None
    tx2.remove(pkg_price)  # leg 2 loses its package price
    isin = news[2].find("t:FinInstrm/t:Id", NS)
    assert isin is not None
    isin.text = "GB0007980592"  # wrong check digit
    bad = Path("report_bad.xml")
    tree.write(bad, encoding="UTF-8", xml_declaration=True)
    print_review(review(xsd, bad, as_of))

    approve(clean, approver="ops.lead@example.co.uk", log=log)
    print("released:", release_for_submission(good, log))
    good.write_text(good.read_text().replace("12.345", "12.346"))  # edited after approval
    try:
        release_for_submission(good, log)
    except PermissionError as exc:
        print("refused:", exc)
    print(f"audit log has {len(log.read_text().splitlines())} chained entries")

Requires xmlschema 4.3.2 and the draft XSD from the FCA's zip file. Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict.

The clean file passes. The damaged copy fails three ways: the schema rejects a TradDt written as +00:00, CON-380 catches the leg that lost its package price, and CON-400 catches the ISIN with a wrong check digit. After a person approves the clean file, release succeeds. When one price is edited from 12.345 to 12.346, release is refused because the hash no longer matches, and the audit log holds three chained entries.

Where does AI fit in a trading AI architecture for reporting?

Not in building the XML. Field mapping, formats and rules are deterministic, and code does them better and more cheaply. The useful work for an agent sits around the exceptions: reading the rule failures, pulling the trade, the order and the reference data for each one, and drafting a short explanation and a suggested fix for the person who owns the correction. The person accepts or changes the fix, and the corrected file goes through the same checks again.

That split matters for model risk. If a model is ever wrong, the schema and rules still stop the file, and a named person still signs it. Our transaction report checks work this way today, comparing the day's reports with trade records before submission.

What breaks in production?

  • **Naive or local timestamps.** A timestamp without a zone, or one written in London time, either fails the schema or reports the wrong instant. Refuse naive datetimes at the edge, as utc_z does.
  • **Package identifiers from upstream systems.** Lower case, hyphens or more than 35 characters fail the schema. Normalise once, in one place, and test it.
  • **Leg prices that drift.** If two systems book the legs of a package, field 38 can differ between them. Compare legs before release.
  • **Schema changes during the consultation.** This is a draft. Pin the XSD you validated against by its hash, record it in the audit log, and rerun the test suite when the FCA publishes a new version.
  • **Approvals of a different file.** People approve what they see on screen. Show the hash and the counts on the approval screen and bind the approval to that hash.
  • **Back-reporting windows.** Until the rules text settles, test both the 3-year and the 5-year boundary.

What are the alternatives to building the checks in-house?

OptionGood fitWatch for
Your approved reporting mechanism's validationFirms that already send through an ARMWhen the ARM will support the draft schema, and if you see the errors before submission
A vendor reporting platformFirms with many asset classes and venuesHow quickly the vendor tracks draft changes, and if you can run their checks on test files now
In-house checks, as aboveFirms that want a pre-submission gate they controlKeeping rules in step with FCA versions, and owning the tests
BothMost firmsDuplicate effort, which is worth it for the cost of a wrong report

Whichever route you take, keep your own record of what was approved and when, and do not treat a vendor's default settings as proof that a control works. Our blog covered a related deadline in trade confirmation automation for T+1.

What should a UK trading firm do before 6 November?

Download the draft zip, generate a handful of your own trades in the new format and run them through the schema. You will find the timestamp, package and client indicator issues within a day. Raise the date-format and back-reporting points in your response, then put the approval and release step in place, because it does not depend on the final schema.

If you want help, our AI Agency UK team can build the pipeline as a 14-day Proof Run on your own sample trades. For a related build, see our worked example on T+1 settlement automation in a bank, and for surveillance settings read trading automation for threshold testing. This is engineering guidance, not regulatory advice. Check the final FCA documents before you rely on any rule described here.

Frequently asked questions

When does the new UK transaction reporting format start?

On 3 April 2028. CP26/34 says FCA systems will switch to the new schema and validation rules on that date, and firms will no longer be able to submit reports in the UK MiFIR formats. The draft schema and rules were published on 2 October 2026 for comment by 6 November 2026.

Why does my timestamp fail the draft schema?

The trading date time must match the pattern .*Z. Python's datetime.isoformat() writes +00:00 for UTC, which does not match. Convert to UTC and format with a literal Z, as the utc_z function in this playbook does, and refuse naive datetimes.

Is the client indicator mandatory?

The draft XSD lets you leave it out, but draft rule CON-080 for the buyer and CON-170 for the seller make it mandatory for reports of trades executed on or after 3 April 2028. Test for it in your own checks, because the schema alone will not catch a missing value.

Should an AI model write transaction reports?

We would not use one to build the XML. The mapping is deterministic and code does it reliably. A model is more useful for explaining rule failures and drafting corrections for a person to review, with the schema, rules and human approval still in place.

References

  1. Financial Conduct Authority, "CP26/34: Preparing for the new transaction reporting regime", 2 October 2026. https://www.fca.org.uk/publications/consultation-papers/cp26-34-preparing-new-transaction-reporting-regime
  2. Financial Conduct Authority, "CP26/34 consultation paper (PDF)", 2 October 2026. https://www.fca.org.uk/publication/consultation/cp26-34.pdf
  3. Financial Conduct Authority, "Draft MDP transaction reporting schema and validation rules V1.3 (zip)", 2 October 2026. https://www.fca.org.uk/publication/systems-information/draft-mdp-transaction-reporting-schema.zip
  4. Financial Conduct Authority, "Market Data Processor: prepare for 2028 transaction reporting changes", 2 October 2026. https://www.fca.org.uk/markets/market-data-processor
  5. Financial Conduct Authority, "PS26/15: Improving the UK transaction reporting regime", 3 August 2026. https://www.fca.org.uk/publications/policy-statements/ps26-15-improving-uk-transaction-reporting-regime
  6. A-Team Insight, "FCA opens consultation on new transaction reporting regime", 6 October 2026. https://a-teaminsight.com/blog/fca-opens-consultation-on-new-transaction-reporting-regime/
  7. PyPI, "xmlschema 4.3.2", 2026. https://pypi.org/project/xmlschema/4.3.2/