Skip to content

Security

MiFID II Transaction Reporting in a Crypto Context: An Engineering Implementation Guide

I implemented the full MiFID II transaction reporting pipeline at Upside. One of the few engineers in crypto who has actually shipped this. Here is the complete guide.

13 min
#mifid2 #compliance #transaction-reporting #regulation #trading #engineering #rts22

In 2019, a tier-2 asset manager in London was fined £976,000 by the FCA for transaction reporting failures. Their algorithmic trading system had been filing reports with incorrect timestamps - not wildly wrong, but offset by a few hundred milliseconds due to a timezone handling bug. They had been trading for three years before the regulator ran a reconciliation. 476,000 transactions had the wrong timestamp format.

The FCA’s position was straightforward: the regulation requires microsecond timestamps synchronized to within 1ms of UTC for algorithmic trading. The firm’s reports had timestamps in local time without UTC offset markers. That was a reportable defect across every single trade.

I implemented the full MiFID II transaction reporting pipeline at Upside, and this experience - the timestamp bug in particular - shaped how I think about compliance infrastructure. The problem is not knowing the rules. The problem is that the rules have sharp edges in exactly the places where trading infrastructure has accumulated technical debt.

What MiFID II RTS 22 Actually Requires

MiFID II Article 26 requires investment firms to report complete and accurate details of every executed transaction to the relevant National Competent Authority (NCA). Regulatory Technical Standard 22 (RTS 22) specifies exactly what “complete and accurate” means.

The report must be submitted by the end of the following business day after execution. For a trade executed on Tuesday, the NCA must receive the report by end of business Wednesday.

The full field list for RTS 22 contains 81+ fields. The ones that are most painful to source from a crypto trading system:

Field 15: Trading venue identifier. Traditional securities have a MIC (Market Identifier Code) - a four-character code like “XLON” for the London Stock Exchange. Crypto exchanges do not have MIC codes. For transactions executed on unregulated venues, RTS 22 uses a special OTC indicator: “XOFF” for off-exchange and “XXXX” for organized trading facilities that do not have a MIC.

Field 31: Price. Price must be reported in the currency of the trade (field 31a) with the appropriate multiplier (field 31b) and notation (field 31c). For crypto-to-crypto trades, you need to determine the reference currency for reporting purposes. If you executed a BTC/ETH trade, you report in your base currency (typically USD) with the exchange rate at execution time. The exchange rate source must be documented and consistently applied.

Fields 44-48: Buyer and seller identification. For transactions involving counterparties, both parties must be identified with their LEI (Legal Entity Identifier) if they have one. Crypto exchanges typically do not have LEIs in the traditional sense. The reporting treatment depends on whether the exchange is acting as a counterparty (and therefore has an LEI obligation) or as an agent.

Field 7: Algorithmic identification. For trades executed by an algorithm, the report must include an identifier for the algorithm that made the trading decision. If the algorithm is modified, a new identifier should be assigned. This creates an audit trail requirement for your strategy code versioning.

The Reporting Pipeline Architecture

The pipeline has four stages: event capture, enrichment, validation, and submission.

Stage 1: Event Capture

Every executed trade must generate an event with enough raw data to populate all 81 RTS 22 fields. The critical fields that must be captured at the point of execution:

  • Execution timestamp (to microsecond precision)
  • Venue identifier (the exchange on which the trade was executed)
  • Instrument identifier (ISIN for traditional securities; for crypto, an internal identifier that maps to a reporting treatment)
  • Quantity executed
  • Price at execution
  • Counterparty identifier if available
  • Strategy/algorithm identifier
  • Order ID chain (from original order through to fill)

The event schema must be immutable at capture. Enrichment happens downstream, but the raw execution event cannot be modified after capture. This is an audit requirement: the report derives from the event, and any post-hoc modification to the event must be versioned and explained.

@dataclass(frozen=True)
class ExecutionEvent:
    execution_id: str
    strategy_id: str
    venue_id: str
    instrument_id: str
    executed_at: datetime  # Must be UTC with microsecond precision
    quantity: Decimal
    price: Decimal
    side: Literal["BUY", "SELL"]
    order_id: str
    capture_ts: datetime  # Wall clock at capture - for audit

The frozen=True dataclass prevents modification after construction. The capture_ts field records when the event was captured by the reporting system, distinct from when it was executed. These will differ slightly - a well-functioning system captures within milliseconds of execution. A large difference (more than a second) should trigger an alert.

Stage 2: Enrichment

The raw execution event does not contain all 81 RTS 22 fields. The enrichment service adds:

  • Instrument reference data. The ISIN (for securities) or the internal instrument mapping to the reporting treatment for crypto assets. Reference data is sourced from ESMA’s Financial Instruments Reference Data System (FIRDS) for traditional instruments, and from your internal taxonomy for crypto.
  • LEI resolution. For each counterparty, look up their LEI in the Global LEI Index (GLEIF) database. This lookup must be cached - the GLEIF API has rate limits and the report submission deadline cannot depend on real-time LEI lookups.
  • Exchange rate. For crypto-to-crypto trades reported in a reference currency, apply the exchange rate from a documented source (typically a reference price from your market data feed at the exact execution timestamp).
  • Reporting firm details. Your own LEI, your investment firm identifier, your regulatory permissions.

Stage 3: Validation

ESMA publishes the validation rules for RTS 22 reports. There are over 800 individual validation rules, grouped into categories. The most commonly failed:

Rule V2013 - Date and time format. All timestamps must be in ISO 8601 UTC format with microsecond precision: 2026-03-30T14:22:31.441000Z. Common failure modes: milliseconds instead of microseconds, local time instead of UTC, missing the trailing Z.

Rule V2030 - ISIN check digit. ISINs have a check digit. If you are constructing ISINs for your crypto instruments, verify the check digit algorithm is correctly implemented. ESMA’s validation rejects reports with invalid ISINs.

Rule V2045 - Quantity and price constraints. Quantity and price must be positive. For SELL orders, the quantity is still reported as a positive number (direction is encoded in the transaction side field, not the quantity sign). This is a common source of sign errors.

Rule V2087 - Algorithm identifier uniqueness. The algorithm identifier in field 7 must be unique per algorithm version per firm. A common error: a single identifier used for a strategy through multiple code versions.

Run every report through the full validation ruleset locally before submission. ESMA provides the validation schema as a downloadable XSD. Any report that fails validation should be quarantined for manual review, not submitted, because rejected reports create a regulatory record of the failure.

Stage 4: Submission to the ARM

Transaction reports are submitted to an Approved Reporting Mechanism (ARM). ARMs are ESMA-authorized entities that receive reports and forward them to the relevant NCA. Major ARMs include CME Group’s regulatory reporting service, DTCC’s GLEIS service, and several specialist regulatory technology providers.

The submission protocol is an HTTPS POST with the report in either XML or CSV format (ARMs accept both). The ARM returns a synchronous acknowledgment for format validation, and an asynchronous acknowledgment for content validation once the NCA accepts the report.

You need to store both acknowledgments. The SOC 2 auditor and the regulator will want to see that every submitted report received a successful NCA acceptance acknowledgment within the required window.

The Timestamp Requirement: RTS 25

RTS 25 is separate from RTS 22 but equally important. It specifies clock synchronization requirements for trading systems.

For algorithmic trading systems: timestamps must be accurate to within 1 millisecond of UTC. The clock must be synchronized using a traceable source (PTP tier-1 or GPS-disciplined clock).

For non-algorithmic trading systems: timestamps must be accurate to within 1 second of UTC. Standard NTP is acceptable.

The implementation at Upside:

For co-located trading systems (colocation at exchange): We used PTP (Precision Time Protocol) with a grandmaster clock provided by the colocation facility. PTP typically achieves nanosecond-level synchronization over a local network. The PTP daemon monitors clock offset and logs any drift event above 100 microseconds.

For cloud-based systems: We used chrony configured with stratum-1 NTP servers. AWS provides an NTP service at 169.254.169.123 synchronized to AWS time servers, which maintain accuracy within 1ms of UTC. We additionally configured external stratum-1 servers for redundancy.

The clock synchronization status must be monitored and alerted on. A system whose clock drifts beyond the 1ms threshold is generating invalid timestamps for all trades from that point until the drift is corrected. At Upside, we monitored clock offset every 60 seconds and paged the on-call engineer if offset exceeded 500 microseconds - giving a buffer before the regulatory threshold was breached.

# Clock monitoring - runs every 60 seconds
def check_clock_offset():
    result = subprocess.run(
        ["chronyc", "tracking"],
        capture_output=True, text=True
    )
    offset_line = next(
        line for line in result.stdout.splitlines()
        if "System time" in line
    )
    # "System time     :   0.000000123 seconds slow of NTP time"
    offset_seconds = float(offset_line.split()[3])
    offset_us = abs(offset_seconds) * 1_000_000

    if offset_us > 500:
        alert(f"Clock offset {offset_us:.1f}μs exceeds 500μs threshold")
    if offset_us > 1000:
        # Pause trading - timestamps no longer compliant
        pause_trading(reason=f"Clock offset {offset_us:.1f}μs exceeds 1ms regulatory limit")

The Reporting Field Schema for a Crypto Spot Trade

This is the structure you would submit to an ARM for a BTC/USD spot purchase executed on a crypto exchange by an algorithmic trading strategy, reported by a MiFID II-authorized investment firm in the EU:

<Transaction>
  <!-- Field 1: Reporting firm -->
  <reportingFirm>
    <lei>2138007QVT8JKXJCA346</lei>
  </reportingFirm>

  <!-- Fields 2-3: Counterparty -->
  <executingFirm>
    <lei>2138007QVT8JKXJCA346</lei>
  </executingFirm>

  <!-- Field 4: Order ID -->
  <orderId>ZC-20260330-00042819</orderId>

  <!-- Field 7: Algorithm identifier -->
  <algorithmId>STRATEGY-ML-ALPHA-V4.2.1</algorithmId>

  <!-- Field 9: Execution datetime -->
  <executionDatetime>2026-03-30T14:22:31.441000Z</executionDatetime>

  <!-- Field 14: Venue -->
  <tradingVenue>XOFF</tradingVenue>  <!-- OTC indicator for unregulated venue -->

  <!-- Fields 16-17: Instrument -->
  <instrumentId type="OTC">
    <instrumentFullName>BITCOIN</instrumentFullName>
    <internalId>BTC-USD-SPOT</internalId>
  </instrumentId>

  <!-- Field 20: Price -->
  <price>82541.50</price>
  <priceCurrency>USD</priceCurrency>

  <!-- Field 21: Quantity -->
  <quantity>0.12300000</quantity>

  <!-- Field 23: Transaction side -->
  <buySellIndicator>BUYI</buySellIndicator>

  <!-- Field 29: Notional value -->
  <notionalAmount>10152.60</notionalAmount>
  <notionalCurrency>USD</notionalCurrency>
</Transaction>

This is a simplified representation. The actual RTS 22 XML schema is more verbose and includes additional validation fields, but this shows the structure that an engineering team needs to generate programmatically for every executed trade.

ADGM/DIFC Mapping for UAE-Based Firms

For firms operating under the Abu Dhabi Global Market (ADGM) or Dubai International Financial Centre (DIFC) regulatory frameworks, the MiFID II equivalent is:

ADGM: The Financial Services Regulatory Authority (FSRA) requires transaction reporting under COBS (Conduct of Business) rules. The reporting format closely follows MiFID II RTS 22 fields but submissions go to the FSRA directly rather than through an EU ARM.

DIFC: The Dubai Financial Services Authority (DFSA) has its own transaction reporting requirements under the DFSA Rulebook. Similar structure to MiFID II but the DFSA publishes its own field specifications.

For a firm regulated in both the EU and the UAE, you will typically need two separate reporting pipelines with slightly different field mappings and submission endpoints. The enrichment stage can be shared; the submission stage must be jurisdictionally separated.

Record-Keeping Requirements Under MiFID II

Transaction reporting is one of two distinct record-keeping obligations under MiFID II. The other is the obligation to maintain records of orders - not just executions. Article 25 of MiFID II requires that investment firms maintain records of all orders and transactions for a minimum of five years.

For algorithmic trading systems, this means:

  • Every order generated by a strategy, whether executed or not
  • Every modification to an order (price updates, quantity changes)
  • Every cancellation, with the reason if available from the exchange
  • Every quote (for firms operating as market makers)

The records must be retrievable within 72 hours of a regulator request. “Retrievable” means formatted and queryable - not raw log files that require custom tooling to parse.

The practical implementation: every order event (create, modify, cancel, fill) is published to an immutable event store. The event store is queryable by order ID, by time range, by strategy, and by instrument. A regulatory request for all orders in a specific instrument over a specific time window can be served within minutes.

The immutability requirement is not optional. If your order records can be modified after the fact, a regulator cannot rely on them. Use an append-only store (AWS DynamoDB with a stream that feeds Kinesis Data Firehose to S3 with object lock enabled, or an immutable logging service like Immudb) for order records.

Testing Your Reporting Pipeline

A reporting pipeline that works in testing but fails in production is worse than a known-broken pipeline - because you believe you are compliant when you are not.

The specific test scenarios that matter:

Rejection rate test. Submit a batch of intentionally invalid reports to your test ARM endpoint and verify that all rejections are logged and none are silently dropped. Many firms have silent discard bugs where validation failures are caught as exceptions and the exception is swallowed.

Late submission simulation. Simulate a processing delay that pushes report submission past end-of-business-day. Verify that your system generates a late submission alert and includes the correct “late submission” flag in the report.

Timezone edge case. Generate a trade at exactly 23:59:59.999999 UTC on a Friday and verify that the report is generated for the correct trade date (Friday, not Saturday) and submitted before end of business on Monday.

Reference data miss. Simulate a trade in an instrument that is not in your reference data cache. Verify that the report is queued for manual review rather than submitted with a blank instrument identifier.

Clock drift simulation. Set your trading server’s clock forward by 2ms and verify that your monitoring detects and alerts on the drift before it breaches the 1ms threshold. This test should be part of your quarterly operational testing schedule.

How This Breaks in Production

The failure mode that the London firm experienced - and that I have seen variants of at multiple firms - is timezone handling in the timestamp pipeline. Every trading system component generates timestamps in a mix of local time, UTC, and Unix timestamps. The conversion between these formats accumulates errors when each component makes different assumptions.

The fix that actually works: all timestamps are Unix microseconds (integer microseconds since the Unix epoch, UTC by definition) from the point of generation to the point of storage. The conversion to ISO 8601 format happens only at the point of serialization for the report. There is one conversion, in one place, in one function that has a unit test that passes a known Unix timestamp and verifies the exact output string.

Never convert timestamp formats in the middle of the reporting pipeline. Convert once, at the output, and test that conversion exhaustively.

The second failure mode is the instrument reference data becoming stale. If your enrichment service uses cached reference data and the cache becomes stale, you will generate reports with incorrect instrument identifiers. Schedule a daily refresh of your reference data and alert if the refresh fails. Do not submit reports using reference data that is more than 48 hours old.

The third failure mode is conflating the submission acknowledgment with the acceptance confirmation. Your ARM will return a synchronous receipt acknowledgment immediately. This confirms only that the report was received in the correct format. The NCA acceptance - the confirmation that the regulator has accepted the report as valid - is asynchronous and may come hours later. Your compliance tracking must wait for the acceptance confirmation before marking a report as successfully filed. A report that was received but rejected by the NCA is a reporting failure, even though the ARM acknowledged receipt.

Continue Reading

Enjoyed this?

Get one deep infrastructure insight per week.

Free forever. Unsubscribe anytime.

You're in. Check your inbox.