FIX.Message
FIX.Message is an Elixir library for framing, parsing, inspecting, and encoding Financial Information eXchange (FIX) messages.
It provides:
- stream-safe framing using
BodyLength(9) CheckSum(10)validation and generation- ordered header, body, and trailer parsing
- byte-exact parsing of length-prefixed data fields, including values containing SOH
- a message struct with commonly used session fields promoted to named attributes
- pipe-delimited display output for logs, inspection, and string interpolation
- preservation of the original wire bytes for message stores, audit logs, and resends
- built-in FIX 4.4 and FIX 5.0 SP2 dictionaries
- extensible dictionaries for counterparty-specific fields
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:
| Field | FIX tag | Representation |
|---|---|---|
begin_string | 8 | binary |
msg_type | 35 | binary |
seq_num | 34 | positive integer |
sender_comp_id | 49 | binary |
target_comp_id | 56 | binary |
sending_time | 52 | binary |
poss_dup_flag | 43 | boolean |
orig_sending_time | 122 | binary |
header | — | unpromoted header fields in wire order |
body | — | body fields in wire order |
raw | — | exact 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:
- standard header and trailer tags
- companion length/data field pairs whose data may contain SOH
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:
:garbled— invalid framing, malformed fields, invalid declared data length, invalid promoted values, or otherwise unparseable input:checksum_mismatch— a complete frame whose checksum does not match
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.