Skip to content

addigence/fix_message

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Contributors

Languages