Skip to content

Commit

Permalink
feat: working connection!
Browse files Browse the repository at this point in the history
  • Loading branch information
EvolveArt committed Jan 8, 2025
1 parent 86dda2f commit 836dc71
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 43 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@ devnet.pkl

# Mac
.DS_STORE

# Lmax connector metadata
lmax-connector/log/
lmax-connector/store/
lmax-connector/config/fix_settings.cfg

199 changes: 199 additions & 0 deletions lmax-connector/config/Fix44.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>

<fix major="4" minor="4" servicepack="0" type="FIX">
<header>
<field name="BeginString" required="Y"/>
<field name="BodyLength" required="Y"/>
<field name="MsgType" required="Y"/>
<field name="SenderCompID" required="Y"/>
<field name="TargetCompID" required="Y"/>
<field name="MsgSeqNum" required="Y"/>
<field name="PossDupFlag" required="N"/>
<field name="PossResend" required="N"/>
<field name="SendingTime" required="Y"/>
<field name="OrigSendingTime" required="N"/>
</header>
<messages>
<message name="Heartbeat" msgcat="admin" msgtype="0">
<field name="TestReqID" required="N"/>
</message>
<message name="TestRequest" msgcat="admin" msgtype="1">
<field name="TestReqID" required="Y"/>
</message>
<message name="ResendRequest" msgcat="admin" msgtype="2">
<field name="BeginSeqNo" required="Y"/>
<field name="EndSeqNo" required="Y"/>
</message>
<message name="Reject" msgcat="admin" msgtype="3">
<field name="RefSeqNum" required="Y"/>
<field name="RefTagID" required="N"/>
<field name="RefMsgType" required="N"/>
<field name="SessionRejectReason" required="N"/>
<field name="Text" required="N"/>
</message>
<message name="SequenceReset" msgcat="admin" msgtype="4">
<field name="GapFillFlag" required="N"/>
<field name="NewSeqNo" required="Y"/>
</message>
<message name="Logout" msgcat="admin" msgtype="5">
<field name="Text" required="N"/>
</message>
<message name="Logon" msgcat="admin" msgtype="A">
<field name="EncryptMethod" required="Y"/>
<field name="HeartBtInt" required="Y"/>
<field name="ResetSeqNumFlag" required="N"/>
<field name="MaxMessageSize" required="N"/>
<field name="Username" required="N"/>
<field name="Password" required="N"/>
</message>
<message name="MarketDataRequest" msgcat="app" msgtype="V">
<field name="MDReqID" required="Y"/>
<field name="SubscriptionRequestType" required="Y"/>
<field name="MarketDepth" required="Y"/>
<field name="MDUpdateType" required="N"/>
<field name="AggregatedBook" required="N"/>
<component name="MDReqGrp" required="Y"/>
<component name="InstrmtMDReqGrp" required="Y"/>
</message>
<message name="MarketDataSnapshotFullRefresh" msgcat="app" msgtype="W">
<field name="MDReqID" required="N"/>
<component name="Instrument" required="Y"/>
<component name="MDFullGrp" required="Y"/>
</message>
<message name="MarketDataRequestReject" msgcat="app" msgtype="Y">
<field name="MDReqID" required="Y"/>
<field name="MDReqRejReason" required="N"/>
<field name="Text" required="N"/>
</message>
</messages>
<trailer>
<field name="CheckSum" required="Y"/>
</trailer>
<components>
<component name="Instrument">
<field name="SecurityID" number="48" type="STRING" required="Y"/>
<field name="SecurityIDSource" number="22" type="STRING" required="Y"/>
</component>
<component name="InstrmtMDReqGrp">
<group name="NoRelatedSym" required="Y">
<component name="Instrument" required="Y"/>
</group>
</component>
<component name="MDReqGrp">
<group name="NoMDEntryTypes" required="Y">
<field name="MDEntryType" number="269" type="CHAR" required="Y"/>
</group>
</component>
<component name="MDFullGrp">
<group name="NoMDEntries" required="Y">
<field name="MDEntryType" number="269" type="CHAR" required="Y"/>
<field name="MDEntryPx" number="270" type="PRICE" required="N"/>
<field name="MDEntrySize" number="271" type="QTY" required="N"/>
<field name="MDEntryDate" number="272" type="UTCDATEONLY" required="N"/>
<field name="MDEntryTime" number="273" type="UTCTIMEONLY" required="N"/>
</group>
</component>
</components>
<fields>
<field name="BeginString" number="8" type="STRING" length="8"/>
<field name="BodyLength" number="9" type="LENGTH" length="3"/>
<field name="MsgType" number="35" type="STRING" length="2">
<value enum="0" description="HEARTBEAT"/>
<value enum="1" description="TESTREQUEST"/>
<value enum="A" description="LOGON"/>
<value enum="2" description="RESENDREQUEST"/>
<value enum="3" description="REJECT"/>
<value enum="4" description="SEQUENCERESET"/>
<value enum="5" description="LOGOUT"/>
<value enum="V" description="MARKETDATAREQUEST"/>
<value enum="W" description="MARKETDATASNAPSHOTFULLREFRESH"/>
<value enum="Y" description="MARKETDATAREJECT"/>
<value enum="xt" description="TRACE_SUBSCRIPTION"/>
</field>
<field name="SenderCompID" number="49" type="STRING"/>
<field name="TargetCompID" number="56" type="STRING"/>
<field name="MsgSeqNum" number="34" type="SEQNUM"/>
<field name="PossDupFlag" number="43" type="BOOLEAN">
<value enum="Y" description="POSSDUP"/>
<value enum="N" description="ORIGTRANS"/>
</field>
<field name="PossResend" number="97" type="BOOLEAN">
<value enum="Y" description="POSSRESEND"/>
<value enum="N" description="ORIGTRANS"/>
</field>
<field name="SendingTime" number="52" type="UTCTIMESTAMP"/>
<field name="OrigSendingTime" number="122" type="UTCTIMESTAMP"/>
<field name="TestReqID" number="112" type="STRING"/>
<field name="BeginSeqNo" number="7" type="SEQNUM"/>
<field name="EndSeqNo" number="16" type="SEQNUM"/>
<field name="RefSeqNum" number="45" type="SEQNUM"/>
<field name="RefTagID" number="371" type="INT"/>
<field name="RefMsgType" number="372" type="STRING"/>
<field name="SessionRejectReason" number="373" type="INT">
<value enum="11" description="INVALIDMSGTYPE"/>
<value enum="99" description="OTHER"/>
<value enum="12" description="XMLVALIDATIONERROR"/>
<value enum="13" description="TAGAPPEARSMORETHANONCE"/>
<value enum="14" description="TAGSPECIFIEDOUTOFREQUIREDORDER"/>
<value enum="15" description="REPEATINGGROUPFIELDSOUTOFORDER"/>
<value enum="16" description="INCORRECTNUMINGROUPCOUNTFORREPEATINGGROUP"/>
<value enum="17" description="NONDATAVALUEINCLUDESFIELDDELIMITERSOHCHARACTER"/>
<value enum="0" description="INVALIDTAGNUMBER"/>
<value enum="1" description="REQUIREDTAGMISSING"/>
<value enum="2" description="TAGNOTDEFINEDFORTHISMESSAGETYPE"/>
<value enum="3" description="UNDEFINEDTAG"/>
<value enum="4" description="TAGSPECIFIEDWITHOUTAVALUE"/>
<value enum="5" description="VALUEISINCORRECTOUTOFRANGEFORTHISTAG"/>
<value enum="6" description="INCORRECTDATAFORMATFORVALUE"/>
<value enum="7" description="DECRYPTIONPROBLEM"/>
<value enum="8" description="SIGNATUREPROBLEM"/>
<value enum="9" description="COMPIDPROBLEM"/>
<value enum="10" description="SENDINGTIMEACCURACYPROBLEM"/>
</field>
<field name="Text" number="58" type="STRING"/>
<field name="GapFillFlag" number="123" type="BOOLEAN">
<value enum="Y" description="GAPFILLMESSAGEMSGSEQNUMFIELDVALID"/>
<value enum="N" description="SEQUENCERESETIGNOREMSGSEQNUMNAFORFIXMLNOTUSED"/>
</field>
<field name="NewSeqNo" number="36" type="SEQNUM"/>
<field name="EncryptMethod" number="98" type="INT">
<value enum="0" description="NONEOTHER"/>
</field>
<field name="HeartBtInt" number="108" type="INT"/>
<field name="ResetSeqNumFlag" number="141" type="BOOLEAN">
<value enum="Y" description="YES"/>
<value enum="N" description="NO"/>
</field>
<field name="MaxMessageSize" number="383" type="LENGTH"/>
<field name="Username" number="553" type="STRING"/>
<field name="Password" number="554" type="STRING"/>
<field name="MDReqID" number="262" type="STRING"/>
<field name="SubscriptionRequestType" number="263" type="CHAR">
<value enum="1" description="SNAPSHOTUPDATE"/>
<value enum="2" description="UNSUBSCRIBE"/>
</field>
<field name="MarketDepth" number="264" type="INT"/>
<field name="MDUpdateType" number="265" type="INT">
<value enum="0" description="FULL"/>
</field>
<field name="AggregatedBook" number="266" type="BOOLEAN"/>
<field name="MDReqRejReason" number="281" type="CHAR"/>
<field name="CheckSum" number="10" type="STRING" length="3"/>
<field name="SecurityID" number="48" type="STRING"/>
<field name="SecurityIDSource" number="22" type="STRING">
<value enum="8" description="EXCHSYMB"/>
</field>
<field name="NoRelatedSym" number="146" type="NUMINGROUP"/>
<field name="NoMDEntryTypes" number="267" type="NUMINGROUP"/>
<field name="MDEntryType" number="269" type="CHAR">
<value enum="0" description="BID"/>
<value enum="1" description="OFFER"/>
<value enum="2" description="TRADE"/>
</field>
<field name="NoMDEntries" number="268" type="NUMINGROUP"/>
<field name="MDEntryPx" number="270" type="PRICE"/>
<field name="MDEntrySize" number="271" type="QTY"/>
<field name="MDEntryDate" number="272" type="UTCDATEONLY"/>
<field name="MDEntryTime" number="273" type="UTCTIMEONLY"/>
</fields>
</fix>
128 changes: 85 additions & 43 deletions lmax-connector/lmax_connector/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Optional
import quickfix as fix
from dotenv import load_dotenv
import shutil

from pragma_sdk.offchain.client import PragmaAPIClient
from pragma_sdk.common.types.pair import Pair
Expand Down Expand Up @@ -33,33 +34,45 @@ def onLogout(self, sessionID):

def fromAdmin(self, message, sessionID):
"""Log admin messages received from LMAX"""
msgType = fix.MsgType()
message.getHeader().getField(msgType)

if msgType.getValue() == fix.MsgType_Reject:
refMsgType = fix.RefMsgType()
message.getField(refMsgType)
refSeqNum = fix.RefSeqNum()
message.getField(refSeqNum)
text = fix.Text()
message.getField(text)
logger.error(f"Message Rejected - Type: {refMsgType.getValue()}, SeqNum: {refSeqNum.getValue()}, Text: {text.getValue()}")
elif msgType.getValue() == fix.MsgType_Logon:
logger.info("Received Logon message")
elif msgType.getValue() == fix.MsgType_Heartbeat:
logger.debug("Received Heartbeat")
else:
logger.info(f"Admin Message - Type: {msgType.getValue()}, Content: {message.toString()}")
try:
msgType = fix.MsgType()
message.getHeader().getField(msgType)

logger.debug(f"Received admin message: {message.toString()}")

if msgType.getValue() == fix.MsgType_Reject:
refMsgType = fix.RefMsgType()
message.getField(refMsgType)
refSeqNum = fix.RefSeqNum()
message.getField(refSeqNum)
text = fix.Text()
message.getField(text)
logger.error(f"Message Rejected - Type: {refMsgType.getValue()}, SeqNum: {refSeqNum.getValue()}, Text: {text.getValue()}")
elif msgType.getValue() == fix.MsgType_Logon:
logger.info("Received Logon message")
elif msgType.getValue() == fix.MsgType_Heartbeat:
logger.debug("Received Heartbeat")
except Exception as e:
logger.error(f"Error processing admin message: {str(e)}, Message: {message.toString()}")

def toAdmin(self, message, sessionID):
"""Log admin messages sent to LMAX"""
msgType = fix.MsgType()
message.getHeader().getField(msgType)

if msgType.getValue() == fix.MsgType_Logon:
logger.info(f"Sending Logon message: {message.toString()}")
else:
logger.debug(f"Sending admin message: {message.toString()}")
try:
msgType = fix.MsgType()
message.getHeader().getField(msgType)

if msgType.getValue() == fix.MsgType_Logon:
# Required fields for LMAX logon
message.setField(fix.EncryptMethod(0)) # No encryption
message.setField(fix.HeartBtInt(30))
message.setField(fix.Username(self.username)) # Tag 553
message.setField(fix.Password(self.password)) # Tag 554
message.setField(fix.ResetSeqNumFlag(True)) # Tag 141
logger.info(f"Sending Logon message: {message.toString()}")
else:
logger.debug(f"Sending admin message: {message.toString()}")
except Exception as e:
logger.error(f"Error preparing admin message: {str(e)}")

def onError(self, sessionID):
"""Log FIX session errors"""
Expand Down Expand Up @@ -121,8 +134,13 @@ def __init__(self, pragma_client: PragmaAPIClient):
self.pragma_client = pragma_client
self.running = True

# Create config directory if it doesn't exist
# Create required directories
os.makedirs("config", exist_ok=True)
os.makedirs("store", exist_ok=True)
os.makedirs("log", exist_ok=True)

# Copy data dictionary
dict_path = "config/FIX44.xml"

# Write FIX settings to file
self.fix_config_path = "config/fix_settings.cfg"
Expand All @@ -143,9 +161,10 @@ def __init__(self, pragma_client: PragmaAPIClient):
BeginString=FIX.4.4
SenderCompID={os.getenv('LMAX_SENDER_COMP_ID')}
TargetCompID={os.getenv('LMAX_TARGET_COMP_ID')}
SocketConnectHost={os.getenv('LMAX_HOST')}
SocketConnectPort={os.getenv('LMAX_PORT')}
SocketConnectHost=127.0.0.1
SocketConnectPort=40003
Password={os.getenv('LMAX_PASSWORD')}
ResetOnLogon=Y
HeartBtInt=30"""

logger.info(f"Using FIX settings:\n{fix_settings}")
Expand All @@ -159,35 +178,58 @@ def init_fix(self):
settings = fix.SessionSettings(self.fix_config_path)
store_factory = fix.FileStoreFactory(settings)
log_factory = fix.FileLogFactory(settings)
self.initiator = fix.SocketInitiator(
self.application,
store_factory,
settings,
log_factory
)
self.initiator.start()

# Initialize application with credentials
self.application.username = os.getenv('LMAX_SENDER_COMP_ID')
self.application.password = os.getenv('LMAX_PASSWORD')

# Start initiator
try:
self.initiator = fix.SocketInitiator(
self.application,
store_factory,
settings,
log_factory
)
self.initiator.start()
logger.info("FIX initiator started successfully")
except Exception as e:
logger.error(f"Failed to start FIX initiator: {str(e)}")
raise

async def subscribe_market_data(self, pair: Pair):
await self.application.session_ready.wait()

symbol = f"{pair.base_currency.id}/{pair.quote_currency.id}"
self.application.market_data_ready[symbol] = asyncio.Event()

# Create market data request based on LMAX example
message = fix.Message()
header = message.getHeader()
header.setField(fix.MsgType(fix.MsgType_MarketDataRequest))

message.setField(fix.MDReqID("1"))
message.setField(fix.SubscriptionRequestType('1')) # Snapshot + Updates
message.setField(fix.MarketDepth(0))
# Required fields as per LMAX example
message.setField(fix.MDReqID("EUR/USD")) # Unique request ID
message.setField(fix.SubscriptionRequestType('1')) # 1 = SNAPSHOTUPDATE
message.setField(fix.MarketDepth(0)) # Full book
message.setField(fix.MDUpdateType(0)) # Full refresh
message.setField(fix.NoMDEntryTypes(2))

# Add entry types group
group = fix.Group(fix.FIELD.NoMDEntryTypes, fix.FIELD.MDEntryType)
group.setField(fix.MDEntryType(fix.MDEntryType_BID))
group.setField(fix.MDEntryType('0')) # Bid
message.addGroup(group)
group.setField(fix.MDEntryType(fix.MDEntryType_OFFER))
group = fix.Group(fix.FIELD.NoMDEntryTypes, fix.FIELD.MDEntryType)
group.setField(fix.MDEntryType('1')) # Offer
message.addGroup(group)

message.setField(fix.Symbol(symbol))
# Add instrument group
message.setField(fix.NoRelatedSym(1))
message.setField(fix.Symbol("4001")) # EUR/USD instrument ID
message.setField(fix.SecurityType("8")) # FX

logger.info(f"Sending market data request: {message.toString()}")
try:
fix.Session.sendToTarget(message)
except fix.RuntimeError as e:
logger.error(f"Failed to send market data request: {str(e)}")

async def push_prices(self, pair: Pair):
symbol = f"{pair.base_currency.id}/{pair.quote_currency.id}"
Expand Down
Loading

0 comments on commit 836dc71

Please sign in to comment.