Minimal, agnostic FIX 4.4 protocol library for Elixir.
ExFixsense provides mechanism, not policy - you control FIX message handling while the library manages protocol requirements.
- ✅ Minimal abstraction - Library handles FIX protocol, you handle business logic
- ✅ Broker agnostic - Works with any FIX 4.4 broker (Cumberland, Coinbase, Binance, etc.)
- ✅ User control - You decide how to handle ResendRequest, Reject, sequence gaps
def deps do
[{:ex_fixsense, "~> 1.0"}]
end# config/config.exs
config :ex_fixsense, :sessions,
my_session: [
host: "fix.broker.com",
port: 9000,
sender_comp_id: "YOUR_SENDER_ID",
target_comp_id: "BROKER",
logon_strategy: ExFixsense.Logon.Standard,
ssl_opts: [verify: :verify_none]
]defmodule MyApp.TradingHandler do
@behaviour ExFixsense.SessionHandler
require Logger
def on_logon(session_key, _config) do
Logger.info("[#{session_key}] Connected!")
# Send your initial requests here
request_market_data(session_key)
end
def on_app_message(session_key, msg, _config) do
# Convert fields to map for easy access
fields = ExFixsense.Protocol.Parser.fields_to_map(msg)
case msg.msg_type do
"W" -> handle_market_data_snapshot(session_key, fields)
"X" -> handle_market_data_update(session_key, fields)
"8" -> handle_execution_report(session_key, fields)
"AP" -> handle_position_report(session_key, fields)
_ -> Logger.debug("[#{session_key}] Unhandled: #{msg.msg_type}")
end
end
def on_session_message(session_key, msg, _config) do
fields = ExFixsense.Protocol.Parser.fields_to_map(msg)
case msg.msg_type do
"0" -> :ok # Heartbeat - library tracks
"1" -> :ok # TestRequest - library auto-responds
"2" ->
# ResendRequest - YOU handle this
end_seq = Map.get(fields, "16")
Logger.warn("[#{session_key}] ResendRequest - sending GapFill")
gap_fill = ExFixsense.Message.Builder.new("4")
|> ExFixsense.Message.Builder.set_field("123", "Y")
|> ExFixsense.Message.Builder.set_field("36", end_seq)
ExFixsense.send_message(session_key, gap_fill)
"3" ->
# Reject - YOU handle this
text = Map.get(fields, "58", "")
Logger.error("[#{session_key}] FIX Reject: #{text}")
"4" -> :ok # SequenceReset - library auto-handles
_ -> :ok
end
end
def on_logout(session_key, reason, _config) do
Logger.warn("[#{session_key}] Disconnected: #{inspect(reason)}")
end
# Your private functions
defp request_market_data(session_key) do
message = ExFixsense.Message.Builder.new("V")
|> ExFixsense.Message.Builder.set_field("262", "MD-#{:os.system_time()}")
|> ExFixsense.Message.Builder.set_field("263", "1") # Subscribe
|> ExFixsense.Message.Builder.set_field("55", "BTC-USD")
ExFixsense.send_message(session_key, message)
end
defp handle_market_data_snapshot(session_key, fields) do
symbol = Map.get(fields, "55")
Logger.info("[#{session_key}] Market data: #{symbol}")
# Update your price store, database, etc.
end
defp handle_execution_report(session_key, fields) do
order_id = Map.get(fields, "11")
status = Map.get(fields, "39")
Logger.info("[#{session_key}] Order #{order_id}: status=#{status}")
end
# ... other handlers
end# In your application supervision tree
{:ok, _pid} = ExFixsense.Core.Session.start_link(
session_key: :my_session,
handler: MyApp.TradingHandler,
handler_state: %{}
)
# Or start directly
ExFixsense.start_session(:my_session, MyApp.TradingHandler)These are handled because FIX specification requires them:
- TestRequest (35=1) → Auto-sends Heartbeat with TestReqID
- SequenceReset (35=4) → Auto-updates recv_seq_num per GapFillFlag
- Logon (35=A) → Sets session status to :logged_on
- Logout (35=5) → Closes socket
- Heartbeat monitoring → Automatic send/receive
- Sequence number tracking → recv_seq_num, send_seq_num
These require business decisions, so you control them:
- ResendRequest (35=2) → Send GapFill or resend messages (requires MessageStore)
- Reject (35=3) → Log error, send alert, disconnect, etc.
- Sequence gaps → Send ResendRequest or disconnect and reconnect
- All application messages → Market data, orders, positions, etc.
Called when session successfully logs on. Send initial requests here.
Example:
def on_logon(session_key, _config) do
# Subscribe to market data
# Request positions
# Send any initial messages
endCalled for all application messages (market data, orders, positions).
Parameters:
session_key- Atom (e.g.,:cumberland_md)msg- %InMessage{} structconfig- Session configuration map
InMessage fields:
msg.msg_type- String ("W", "8", "AP", etc.)msg.seqnum- Integer (sequence number)msg.fields- List of tuples:[{"55", "BTC-USD"}, {"270", "50000.00"}, ...]msg.original_fix_msg- Binary (raw FIX message)msg.valid- Booleanmsg.poss_dup- Boolean (PossDupFlag)
Convert to map for easy access:
fields = ExFixsense.Protocol.Parser.fields_to_map(msg)
symbol = Map.get(fields, "55") # Tag 55 = Symbol
price = Map.get(fields, "270") # Tag 270 = MDEntryPxCalled for session messages you must handle:
- ResendRequest (35=2)
- Reject (35=3)
- Sequence gaps (when msg.seqnum > expected)
Also receives (but library auto-handles):
- Heartbeat (35=0)
- TestRequest (35=1)
- SequenceReset (35=4)
Example:
def on_session_message(session_key, msg, _config) do
fields = ExFixsense.Protocol.Parser.fields_to_map(msg)
case msg.msg_type do
"2" -> handle_resend_request(session_key, fields)
"3" -> handle_reject(session_key, fields)
_ -> :ok
end
endCalled when session disconnects. Clean up resources here.
Example:
def on_logout(session_key, reason, _config) do
Logger.warn("Session #{session_key} disconnected: #{inspect(reason)}")
# Clean up, notify other parts of app, etc.
endUse fluent API to build FIX messages:
# Market data subscription
message = ExFixsense.Message.Builder.new("V") # MarketDataRequest
|> ExFixsense.Message.Builder.set_field("262", "MD-#{:os.system_time()}") # MDReqID
|> ExFixsense.Message.Builder.set_field("263", "1") # Subscribe
|> ExFixsense.Message.Builder.set_field("264", "1") # Full book
|> ExFixsense.Message.Builder.set_field("55", "BTC-USD") # Symbol
ExFixsense.send_message(:my_session, message)
# New order
order = ExFixsense.Message.Builder.new("D") # NewOrderSingle
|> ExFixsense.Message.Builder.set_field("11", "ORDER-#{:os.system_time()}") # ClOrdID
|> ExFixsense.Message.Builder.set_field("55", "BTC-USD") # Symbol
|> ExFixsense.Message.Builder.set_field("54", "1") # Side (Buy)
|> ExFixsense.Message.Builder.set_field("38", "0.5") # Quantity
|> ExFixsense.Message.Builder.set_field("40", "2") # OrdType (Limit)
|> ExFixsense.Message.Builder.set_field("44", "50000.00") # Price
ExFixsense.send_message(:my_session, order)Repeating groups (same tag multiple times):
message = ExFixsense.Message.Builder.new("V")
|> ExFixsense.Message.Builder.set_field("267", "2") # NoMDEntryTypes
|> ExFixsense.Message.Builder.set_field("269", "0") # Bid
|> ExFixsense.Message.Builder.set_field("269", "1") # Offer
# Results in: %{"269" => ["0", "1"]}config :ex_fixsense, :sessions,
my_broker: [
host: "fix.broker.com",
port: 9000,
sender_comp_id: "YOUR_SENDER_COMP_ID",
sender_sub_id: "YOUR_SENDER_SUB_ID",
target_comp_id: "BROKER",
logon_strategy: ExFixsense.Logon.OnBehalfOf,
logon_fields: %{
on_behalf_of_comp_id: "YOUR_COUNTERPARTY_ID",
on_behalf_of_sub_id: "YOUR_USER_ID"
},
ssl_opts: [
certfile: "path/to/client.crt",
keyfile: "path/to/client.key",
cacertfile: "path/to/ca.crt",
verify: :verify_peer
]
]Note: For OnBehalfOf, you must manually add Tag 115/116 to application messages:
# Get OnBehalfOf credentials from config
on_behalf_of_comp_id = config.logon_fields.on_behalf_of_comp_id
on_behalf_of_sub_id = config.logon_fields.on_behalf_of_sub_id
# Add to EVERY application message
message = ExFixsense.Message.Builder.new("V")
|> ExFixsense.Message.Builder.set_field("115", on_behalf_of_comp_id) # OnBehalfOfCompID
|> ExFixsense.Message.Builder.set_field("116", on_behalf_of_sub_id) # OnBehalfOfSubID
|> ExFixsense.Message.Builder.set_field("262", "MD-#{:os.system_time()}")
# ... other fieldsconfig :ex_fixsense, :sessions,
coinbase: [
host: "fix.coinbase.com",
port: 4198,
sender_comp_id: System.get_env("COINBASE_API_KEY"),
target_comp_id: "Coinbase",
logon_strategy: ExFixsense.Logon.UsernamePassword,
logon_fields: %{
username: System.get_env("COINBASE_USERNAME"),
password: System.get_env("COINBASE_PASSWORD")
},
ssl_opts: [verify: :verify_peer]
]config :ex_fixsense, :sessions,
generic: [
host: "fix.broker.com",
port: 9000,
sender_comp_id: "YOUR_ID",
target_comp_id: "BROKER",
logon_strategy: ExFixsense.Logon.Standard,
ssl_opts: [verify: :verify_none] # Use verify_peer in production!
]Connect to multiple brokers simultaneously:
# Configure multiple sessions
config :ex_fixsense, :sessions,
broker_a: [...],
broker_b: [...],
broker_c: [...]
# Start all sessions
children = [
{ExFixsense.Core.Session, session_key: :broker_a, handler: MyApp.Handler},
{ExFixsense.Core.Session, session_key: :broker_b, handler: MyApp.Handler},
{ExFixsense.Core.Session, session_key: :broker_c, handler: MyApp.Handler}
]
Supervisor.start_link(children, strategy: :one_for_one)
# Your handler receives session_key to identify which broker
def on_app_message(session_key, msg, _config) do
case session_key do
:broker_a -> handle_broker_a(msg)
:broker_b -> handle_broker_b(msg)
:broker_c -> handle_broker_c(msg)
end
endmix test # Run all tests
mix test --cover # Run with coverageCurrent status: 119 tests, 0 failures ✅
ExFixsense.start_session/2,3- Start FIX sessionExFixsense.send_message/2- Send FIX messageExFixsense.stop_session/1- Stop session gracefully
ExFixsense.Logon.Standard- Minimal authenticationExFixsense.Logon.UsernamePassword- Tag 553/554ExFixsense.Logon.OnBehalfOf- Tag 115/116
Builder.new/1- Create messageBuilder.set_field/3- Add fieldBuilder.get_field/2- Get field valueBuilder.has_field?/2- Check field exists
Parser.validate_and_parse/1- Parse FIX message → %InMessage{}Parser.fields_to_map/1- Convert fields list → map
Common message types:
A= Logon5= Logout0= Heartbeat1= TestRequest2= ResendRequest3= Reject4= SequenceReset
D= NewOrderSingle8= ExecutionReport9= OrderCancelRejectV= MarketDataRequestW= MarketDataSnapshotFullRefreshX= MarketDataIncrementalRefreshY= MarketDataRequestRejectx= SecurityListRequesty= SecurityListAN= PositionMaintenanceRequestAP= PositionReportAO= PositionReportAck
- Check host/port in config
- Verify network connectivity
- Check broker server is running
- Verify certificate paths
- Check certificates not expired
- Use
verify: :verify_nonefor testing (NOT production!)
- Verify SenderCompID and TargetCompID
- Check credentials
- Ensure logon_strategy matches broker requirements
If you don't see any FIX logs in your console:
-
Check Logger level - Must be
:debugor:infoto see connection/request logs# config/dev.exs config :logger, level: :debug
-
Test Logger works
iex> require Logger iex> Logger.debug("Test debug") iex> Logger.info("Test info") iex> Logger.warn("Test warn") iex> Logger.error("Test error")
If you see these messages, Logger is working. If not, check your logger backends.
-
Filter logs by session in terminal
# Filter by session key prefix tail -f log/dev.log | grep "\[cumberland" # Or watch in real-time iex -S mix | grep -i "FIX"
-
Change level at runtime
iex> Logger.configure(level: :debug) # Show all logs iex> Logger.configure(level: :info) # Hide debug, show info/warn/error
Common issue: Logger level set to :warning only shows warnings and errors, hiding :info and :debug logs.
If you receive ResendRequest (35=2), you must respond with:
- GapFill (recommended if MessageStore not implemented)
- Resend messages (requires MessageStore)
Example GapFill:
gap_fill = ExFixsense.Message.Builder.new("4")
|> ExFixsense.Message.Builder.set_field("123", "Y") # GapFillFlag
|> ExFixsense.Message.Builder.set_field("36", end_seqnum) # NewSeqNo
ExFixsense.send_message(session_key, gap_fill)Prevent handler errors from crashing session by wrapping both callbacks in try/rescue:
def on_session_message(session_key, msg, _config) do
try do
handle_session_message(session_key, msg)
rescue
e ->
Logger.error("[#{session_key}] Session message handler error for #{msg.msg_type}: #{Exception.message(e)}")
Logger.debug("[#{session_key}] Stacktrace: #{Exception.format_stacktrace(__STACKTRACE__)}")
:ok # Don't crash session
end
end
def on_app_message(session_key, msg, _config) do
try do
handle_app_message(session_key, msg)
rescue
e ->
Logger.error("[#{session_key}] Handler error for #{msg.msg_type}: #{Exception.message(e)}")
Logger.debug("[#{session_key}] Stacktrace: #{Exception.format_stacktrace(__STACKTRACE__)}")
:ok # Don't crash session
end
end
defp handle_session_message(session_key, msg) do
# Your session message logic (ResendRequest, Reject, etc.)
end
defp handle_app_message(session_key, msg) do
# Your application message routing logic
endImportant: Protect both on_session_message and on_app_message to ensure session stability.
Handlers are stateless callbacks. For persistent state, use:
- Agent - Simple key-value store
- GenServer - Complex state management
- ETS - High-performance shared state
See examples/USAGE_EXAMPLE.md for patterns.
Check session health:
case Registry.lookup(ExFixsense.SessionRegistry, :my_session) do
[{pid, _}] when is_pid(pid) -> Process.alive?(pid)
_ -> false
endConfigure Logger to see FIX session events:
# config/dev.exs
config :logger,
level: :debug # Show all FIX logs
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id],
level: :debug# config/prod.exs
config :logger,
level: :warning # Only warnings and errorsLogger.info- Connection events (on_logon)Logger.warn- ResendRequest, disconnectionsLogger.error- Reject messages, handler errorsLogger.debug- Request messages, unhandled messages, stacktraces
Tip: Include [session_key] prefix in all logs for easy filtering:
Logger.info("[#{session_key}] Connected to #{config.host}:#{config.port}")
Logger.error("[#{session_key}] Handler error for #{msg.msg_type}: #{Exception.message(e)}")Runtime log level change:
iex> Logger.configure(level: :debug) # Show everything
iex> Logger.configure(level: :info) # Hide debug logsMIT License - see LICENSE
- Documentation: hexdocs.pm/ex_fixsense
- FIX Specification: fixtrading.org
- GitHub: github.com/liharsw/ex_fixsense
Built with ❤️ using Elixir and OTP