FIX.Message

FIX.Message is an Elixir library for framing, parsing, inspecting, and encoding Financial Information eXchange (FIX) messages.

It provides:

Installation

Add fix_message to your dependencies in mix.exs:

def deps do
[
{:fix_message, "~> 0.1.0"}
]
end

Message API

FIX.Message is the primary high-level API. It promotes canonical session fields onto a struct while preserving all other fields as ordered {tag, value} tuples.

Build and encode a message

message = %FIX.Message{
begin_string: "FIX.4.4",
msg_type: "D",
seq_num: 42,
sender_comp_id: "SENDER",
target_comp_id: "TARGET",
sending_time: "20260727-12:30:00.000",
header: [{115, "ON_BEHALF_OF"}],
body: [
{11, "ORDER-1"},
{54, "1"},
{38, "100"},
{55, "AAPL"},
{40, "1"}
]
}
wire_message = FIX.Message.to_fix(message)

to_fix/1 computes BodyLength(9) and CheckSum(10). begin_string and msg_type are required; optional promoted fields with nil values are omitted.

Display a message

Use FIX.Message.to_string/1 for conventional, human-readable FIX output with | delimiters:

FIX.Message.to_string(message)
# => "8=FIX.4.4|9=...|35=D|49=SENDER|56=TARGET|34=42|...|10=...|"
"sending: #{message}"
# String interpolation uses the same display form.

Display output is intended for logs and debugging; it is not valid FIX wire data. Use FIX.Message.to_fix/1 when sending or persisting an encoded message.

Unlike to_fix/1, to_string/1 also accepts incomplete message structs. In that case it renders the available fields without derived BodyLength(9) or CheckSum(10) fields:

FIX.Message.to_string(%FIX.Message{msg_type: "0", seq_num: 1})
# => "35=0|34=1|"

Because field values can themselves contain |, display output is not guaranteed to be reversible. FIX.Message implements String.Chars, so Kernel.to_string/1 and string interpolation delegate to this display function.

Parse a message

case FIX.Message.parse(buffer) do
{:ok, message, rest} ->
IO.inspect(message.msg_type)
IO.inspect(message.seq_num)
IO.inspect(message.body)
# Continue parsing any pipelined data in `rest`.
:incomplete ->
# Read more bytes and append them to the buffer.
{:error, :checksum_mismatch} ->
# The frame was complete, but CheckSum(10) was invalid.
{:error, :garbled} ->
# The buffer was not a valid FIX message.
end

A parsed message contains:

FieldFIX tagRepresentation
begin_string8binary
msg_type35binary
seq_num34positive integer
sender_comp_id49binary
target_comp_id56binary
sending_time52binary
poss_dup_flag43boolean
orig_sending_time122binary
headerunpromoted header fields in wire order
bodybody fields in wire order
rawexact bytes consumed from the input

BodyLength(9) and CheckSum(10) are validated but are not stored. Wire encoding with to_fix/1 always uses the current struct data and ignores raw.

Values remain binaries unless explicitly promoted to another type. This preserves formatting and precision, particularly for timestamps and numeric FIX values.

Parsing byte streams

Both FIX.Message.parse/1 and FIX.Parser.parse_message/1 consume exactly one message from the front of a buffer and return the remaining bytes. This makes them suitable for TCP streams and pipelined messages:

def parse_available(buffer, messages \\ []) do
case FIX.Message.parse(buffer) do
{:ok, message, rest} -> parse_available(rest, [message | messages])
:incomplete -> {:ok, Enum.reverse(messages), buffer}
{:error, reason} -> {:error, reason}
end
end

The original bytes consumed for each high-level message are retained in message.raw.

Low-level parser

Use FIX.Parser when you need framing or tokenized sections without constructing a FIX.Message.

Frame one message

FIX.Parser.frame(buffer)
# => {:ok, complete_wire_message, rest}
# => :incomplete
# => {:error, :garbled | :checksum_mismatch}

Frame and tokenize

FIX.Parser.parse_message(buffer)
# => {:ok, {header, body, trailer}, rest}

Each section is an ordered list of {integer_tag, binary_value} tuples. Section classification is positional: after the first body field, later header tags remain in the body; after the trailer begins, all remaining fields stay in the trailer.

Tokenize fields only

soh = <<1>>
FIX.Parser.parse("35=D" <> soh <> "11=ORDER-1" <> soh)
# => {:ok, {[{35, "D"}], [{11, "ORDER-1"}], []}}

parse/2 tokenizes fields but does not validate BodyLength(9) or CheckSum(10). Use parse_message/2, frame/1, or FIX.Message.parse/2 when validating a complete wire message.

Dictionaries

The parser uses dictionaries to identify:

The default dictionary is FIX.Dictionary.FIX44. FIX 5.0 SP2 and FIXT 1.1 session fields are available through FIX.Dictionary.FIX50SP2:

FIX.Message.parse(buffer, FIX.Dictionary.FIX50SP2)
FIX.Parser.parse_message(buffer, FIX.Dictionary.FIX50SP2)
FIX.Parser.parse(fields, FIX.Dictionary.FIX50SP2)

Custom dictionaries

Extend a standard dictionary for bilateral or counterparty-defined fields:

defmodule MyBroker.Dictionary do
use FIX.Dictionary, extends: FIX.Dictionary.FIX44
data_field 5001, 5002
header_field 5003
trailer_field 5004
end

Then pass the module to the parser:

FIX.Message.parse(buffer, MyBroker.Dictionary)

data_field length_tag, data_tag declares that the data field must immediately follow its length field and contain exactly the declared number of bytes. Local declarations take precedence over the extended dictionary.

Length-prefixed data

FIX data fields such as RawData(96) may contain arbitrary bytes, including SOH. The parser uses the preceding companion length field—RawDataLength(95) in this case—to extract the value byte-exactly:

soh = <<1>>
data = "left" <> soh <> "right"
fields = "95=#{byte_size(data)}" <> soh <> "96=" <> data <> soh
FIX.Parser.parse(fields)
# => {:ok, {[], [{95, "10"}, {96, data}], []}}

When constructing messages, include both the length field and data field in the correct order; the encoder only derives BodyLength(9) and CheckSum(10).

Supported errors

Complete-message parsing returns two error reasons:

A valid prefix of a message returns :incomplete, allowing the caller to read and append more bytes.

Development

Run the test suite with:

mix test

License

Licensed under the Apache License 2.0. See LICENSE.