Low-latency Binance FIX protocol bot with ED25519 auth, defensive parsing, and a spread market-making loop
- The Problem
- Features
- Tech Stack
- Architecture
- Getting Started
- Usage
- How It Works
- Architectural Decisions
- Project Structure
- Testing
- Related Projects
- License
- Author
FIX protocol is the industry standard for trading latency, but integrating it directly requires managing three concurrent session types (market data, order entry, drop copy) over TLS with asymmetric authentication. Most off-the-shelf FIX SDKs crash on malformed market-data messages, and switching between REST and FIX adds operational friction for strategies that need to iterate on pricing.
This bot provides a complete, production-ready FIX architecture for Binance spot trading: three independent TLS sessions managed by a defensive parser that skips corrupt fields instead of crashing, ED25519 signing for non-expiring keyless auth, and a configurable spread market-making loop that cycles through tick events. The design prioritizes robustness (graceful degradation) and observability (detailed logging) over feature breadth.
- Three concurrent FIX sessions - Market Data, Order Entry, Drop Copy on independent connections
- ED25519 logon auth - non-expiring asymmetric signature, no shared-secret API key on the wire
- Defensive FIX parser - gracefully skips malformed tag-value fields that crash the official SDK on certain symbols
- Spread market-making loop - places paired BUY/SELL quotes around best bid/ask, replaces on stale or drift
- Tunable strategy - spread %, stale threshold, qty, and log level all CLI-configurable
- Order-lifecycle aware - distinguishes pending/new/filled and cycles only when both sides clear
| Component | Technology |
|---|---|
| Language | Python 3.10+ |
| Package manager | uv |
| FIX library | simplefix |
| FIX SDK | Forked binance-fix-connector-python (defensive-parse mods) |
| Crypto | cryptography (ED25519 signing) |
| Config | python-dotenv + optional config.ini |
| Tests | pytest, pytest-asyncio |
| FIX version | FIX.4.4 |
graph TD
subgraph "Binance FIX endpoints"
MD["Market Data<br/>fix-md.binance.com:9000"]
OE["Order Entry<br/>fix-oe.binance.com:9000"]
DC["Drop Copy<br/>fix-dc.binance.com:9000"]
end
subgraph "FIX Connector (modified SDK)"
CONN["BinanceFixConnector"]
PARSER["Defensive parser<br/>parse_server_response"]
AUTH["ED25519 signer"]
end
subgraph "Bot core"
BOT["SpreadMMBot"]
OM["OrderManagement"]
QUEUE["Message queue<br/>(threaded receiver)"]
end
MD --> CONN
OE --> CONN
DC --> CONN
CONN --> PARSER
PARSER --> QUEUE
QUEUE --> BOT
BOT --> OM
OM --> AUTH
AUTH --> CONN
style MD fill:#0f3460,color:#fff
style OE fill:#0f3460,color:#fff
style DC fill:#0f3460,color:#fff
style CONN fill:#16213e,color:#fff
style PARSER fill:#533483,color:#fff
style AUTH fill:#533483,color:#fff
style BOT fill:#16213e,color:#fff
style OM fill:#16213e,color:#fff
style QUEUE fill:#0f3460,color:#fff
- Python 3.10+
uv- see install instructions- A Binance account with ED25519 API keys configured (Account -> API Management -> Edit -> ED25519)
- ED25519 private key as a PEM file (saved to
secrets/ed25519_private.pemby convention)
git clone https://github.com/adityonugrohoid/ratu-fix-bot.git
cd ratu-fix-bot
uv syncThe bundled src/binance_fix_connector/ is the modified SDK. To rebuild from upstream:
git clone https://github.com/binance/binance-fix-connector-python.git
cp -r binance-fix-connector-python/binance_fix_connector ./src/binance_fix_connector
# Re-apply the defensive-parse patch from src/binance_fix_connector/fix_connector.pyThe bot accepts credentials in either of two forms (checked in order):
Option A - environment variables (.env):
cp .env.example .env
# Edit .env:
# BINANCE_ED25519_API_KEY=your_api_key_here
# BINANCE_ED25519_PRIV_PATH=secrets/ed25519_private.pemOption B - config.ini:
cp examples/config.ini.example config.ini
# Edit config.ini:
# [keys]
# API_KEY = your_api_key_here
# PATH_TO_PRIVATE_KEY_PEM_FILE = secrets/ed25519_private.pem# Default config - reads .env, runs spread-MM on ETHFDUSD
uv run ratu-fix-bot
# Explicit config file
uv run ratu-fix-bot --config config.ini
# Override CLI parameters
uv run ratu-fix-bot --symbol BTCUSDT --qty 0.001 --spread 0.05 --stale-threshold 5
# Help
uv run ratu-fix-bot --help
# Standalone examples (use the connector directly)
uv run python examples/trade/new_order.py
uv run python examples/maket_stream/ticker_stream.py2025-12-13 10:12:26 INFO FIX Client: Connected to tcp+tls://fix-md.binance.com:9000
2025-12-13 10:12:26 INFO LOGIN (A)
2025-12-13 10:12:26 INFO Client=>Server: 8=FIX.4.4|9=251|35=A|49=BMDWATCH|56=SPOT|34=1|...
2025-12-13 10:12:26 INFO MD session established
2025-12-13 10:12:27 INFO OE session established
2025-12-13 10:12:27 INFO Sent InstrumentListRequest for ETHFDUSD
2025-12-13 10:12:27 INFO Validated: MinQty=0.0001, MinPriceInc=0.01
2025-12-13 10:12:27 INFO Subscribed to ETHFDUSD ticker stream
2025-12-13 10:12:28 INFO Stale or missing orders detected: Bid=3092.48, Ask=3092.7
2025-12-13 10:12:29 INFO Placed BUY order: ClOrdID=buy_..., Price=3092.29
2025-12-13 10:12:29 INFO Placed SELL order: ClOrdID=sell_..., Price=3092.83
2025-12-13 10:12:29 INFO Order confirmed: Status=New
2025-12-13 10:12:30 INFO Quote orders placed, awaiting further action
FIX messages use
|as field separator. Key tags:35=A(Logon),35=D(NewOrderSingle),35=V(MarketDataRequest),35=8(ExecutionReport),54=1/2(Buy/Sell).
Each session is a long-lived TLS socket maintained by BinanceFixConnector:
| Session | Endpoint | Purpose |
|---|---|---|
| Market Data | tcp+tls://fix-md.binance.com:9000 |
Subscribe to ticker / book / trade streams |
| Order Entry | tcp+tls://fix-oe.binance.com:9000 |
Send NewOrderSingle, receive ExecutionReport |
| Drop Copy | tcp+tls://fix-dc.binance.com:9000 |
Independent fill confirmations (audit trail) |
A background receiver thread reads from each socket into a shared queue; the bot consumes from the queue without blocking on I/O.
The Logon message (35=A) carries the request signed with the local ED25519 private key. Binance verifies with the public key registered in the user's API settings. No shared secret traverses the wire and the keypair never expires.
On every tick of the market-data stream, the bot compares its outstanding orders against the live best bid/ask. If either side is stale (older than --stale-threshold seconds) or has drifted off the desired spread, it cancels and re-places at the new price. When both sides fill within the same cycle, the loop resets.
Quote pricing formula:
spread_offset = spread_percent / 200 # 0.01% -> 0.00005
buy_price = current_bid * (1 - spread_offset) # below best bid
sell_price = current_ask * (1 + spread_offset) # above best askCLI parameters:
| Parameter | Default | Description |
|---|---|---|
--symbol |
ETHFDUSD |
Trading pair |
--spread |
0.01 |
Spread offset % (0.01 = 0.01%) |
--stale-threshold |
2 |
Seconds before refreshing quotes |
--qty |
0.002 |
Order quantity per side |
--log-level |
INFO |
DEBUG / INFO / WARNING / ERROR |
The original Binance SDK parser raises on malformed tag-value fields that occasionally appear in market-data symbols. The fork in src/binance_fix_connector/fix_connector.py skips those fields and continues, logging once for visibility:
# Malformed tag-value field detection (skip and log)
malformed = [
s for s in tag_values
if '=' not in s or (s.startswith('8=') and s != f'8={self.fix_version}')
]
if malformed:
continue # Do not process this message
# Complete-message check with exception handling
try:
fix_msg = FixMessage()
fix_msg.append_strings([s for s in tag_values if '=' in s])
messages.append(fix_msg)
except Exception:
continue # Skip this message on errorDecision: Use Binance's FIX endpoints rather than REST or the WebSocket Streams API.
Reasoning: FIX gives sub-millisecond order entry latency and a session-level audit trail (Drop Copy). For a market-maker that re-quotes on tick, even small per-message overhead compounds across thousands of cycles per minute.
Decision: Use Binance's ED25519 logon auth instead of the older HMAC-signed REST flow.
Reasoning: No shared secret on the wire, no expiring keys, and rotation just means publishing a new public key in the account settings. The cost is one extra setup step (PEM file) - negligible for the security gain.
Decision: Maintain a local fork of the Binance FIX SDK rather than monkey-patching at runtime.
Reasoning: The patch touches a hot path (parse_server_response); a runtime monkeypatch would obscure stack traces and make CI / type-checking lie. A vendored fork keeps the modification visible and reviewable in git.
Decision: Receive FIX messages on a daemon thread and hand to the strategy via queue.
Reasoning: The official SDK is sync; rewriting it to async would multiply the maintenance burden. A single dedicated receiver thread gives the same non-blocking semantics for the strategy loop without dragging in asyncio complexity.
ratu-fix-bot/
├── src/
│ ├── binance_fix_connector/ # Forked Binance FIX SDK with defensive parser
│ │ ├── fix_connector.py # parse_server_response (modified)
│ │ └── utils.py # Key loading helpers
│ └── ratu_fix_bot/ # Bot package
│ ├── core/
│ │ ├── bot.py # SpreadMMBot
│ │ ├── market_data.py # Market-data subscription + tick loop
│ │ ├── order_management.py # Place / cancel / replace
│ │ └── session.py # FIX session lifecycle
│ ├── config.py # Bot configuration loader
│ └── main.py # CLI entrypoint
├── examples/ # Direct connector usage (no bot)
│ ├── trade/ # new_order.py, new_list_OTO_order.py
│ ├── maket_stream/ # ticker_stream.py, depth_stream.py, trade_stream.py
│ └── general/ # instrument_list.py, current_messages_limit_rate.py
├── tests/
│ ├── test_fix_connector.py
│ ├── trade/ # New-order / OTO unit tests
│ ├── market_stream/ # Stream parser tests
│ ├── test_ratu_fix_bot/ # Bot-level unit tests
│ └── general/ # Rate-limit + instrument-list flows
├── .env.example
└── pyproject.toml
uv run pytest -v| Module | Coverage |
|---|---|
tests/test_fix_connector.py |
Connector unit tests, defensive-parse cases |
tests/trade/ |
NewOrderSingle and OTO order construction |
tests/market_stream/ |
Book ticker / depth / trade stream parsing |
tests/test_ratu_fix_bot/ |
Bot config + lifecycle |
tests/general/ |
Rate-limit + instrument-list flows |
Test bench includes a checked-in unit-test ED25519 key (
tests/unit_test_key.pem) - synthetic only, never used against a live account.
| Project | Description |
|---|---|
| ratu-template | Opinionated Python scaffold for real-time, event-driven trading and monitoring systems |
| ratu-moon-radar | Multi-chain DEX pair scanner and trending-token detector via Moralis API |
| ratu-onchain-monitor | On-chain token holder analytics and whale tracker via Ankr API |
| ratu-rest-api | Binance market snapshot client for price, depth, trades, and multi-timeframe klines |
This project is licensed under the MIT License.
Adityo Nugroho (@adityonugrohoid)