|
1 | 1 | import asyncio |
2 | 2 | import logging |
3 | 3 | import os |
4 | | -import socket |
5 | 4 | import ssl |
6 | 5 | import sys |
7 | | -import threading |
8 | | -import time |
| 6 | +from websockets import connect |
9 | 7 |
|
10 | 8 | from dotenv import load_dotenv |
11 | 9 |
|
12 | 10 | from messages import ( |
13 | | - checkMsg, |
14 | 11 | checkLogonMsg, |
15 | 12 | getMsgCancel, |
16 | 13 | getMsgHeartbeat, |
17 | 14 | getMsgLogon, |
18 | 15 | getMsgNewOrder, |
19 | | - translateFix, |
20 | 16 | ) |
21 | | -from utils import get_attr, get_log_filename |
| 17 | +from utils import get_log_filename |
22 | 18 |
|
23 | 19 | # Common settings |
24 | 20 | SEPARATOR = "\x01" |
|
47 | 43 | # Add file handler to the logger |
48 | 44 | logger.addHandler(file_handler) |
49 | 45 |
|
50 | | -# first seqnum taken by LOGON message, this var is incremented for new orders, heartbeat |
| 46 | +# first seqnum taken by LOGON message, incremented for new orders and heartbeats |
51 | 47 | seqnum = 2 |
52 | | - |
53 | | -# Event object to signal the heartbeat thread to stop |
54 | | -stop_event = threading.Event() |
55 | | - |
56 | | - |
57 | | -def send_heartbeat(apikey, conn): |
| 48 | +async def send_heartbeat(ws, apikey: str) -> None: |
| 49 | + """Periodically send FIX heartbeat messages over the websocket.""" |
58 | 50 | global seqnum |
59 | | - seqnum += 1 |
60 | | - try: |
| 51 | + init_sleep = int(os.getenv("INIT_SLEEP", 60)) |
| 52 | + await asyncio.sleep(init_sleep) |
| 53 | + heartbeat_sleep = int(os.getenv("HEARTBEAT_SLEEP", 90)) |
| 54 | + while True: |
| 55 | + await asyncio.sleep(heartbeat_sleep) |
| 56 | + seqnum += 1 |
61 | 57 | msg = getMsgHeartbeat(apikey, seqnum) |
62 | | - conn.sendall(msg) |
63 | | - logger.info(f"Sending Heartbeat Msg: {msg}") |
64 | | - logger.info(f"Sent heartbeat message with Success @ seqnum {seqnum}") |
65 | | - except Exception as e: |
66 | | - logger.error(f"Failed to send Heartbeat message: error was '{e}'") |
67 | | - |
68 | | - |
69 | | -def heartbeat_thread(apikey, conn, stop_event): |
70 | | - try: |
71 | | - INIT_SLEEP = os.getenv( |
72 | | - "INIT_SLEEP", 60 |
73 | | - ) # SLEEP for X seconds while client is starting up, default to 60 seconds |
74 | | - time.sleep(INIT_SLEEP) |
75 | | - HEARTBEAT_SLEEP = int(os.getenv("HEARTBEAT_SLEEP", 90)) # defaults to 90 secs |
76 | | - while not stop_event.is_set(): |
77 | | - # delay start of thread by 20 secs |
78 | | - send_heartbeat(apikey, conn) |
79 | | - time.sleep( |
80 | | - HEARTBEAT_SLEEP |
81 | | - ) # Send heartbeat every `HEARTBEAT_SLEEP` seconds |
82 | | - except Exception as e: |
83 | | - print(f"Heartbeat thread exception: {e}") |
| 58 | + await ws.send(msg) |
| 59 | + logger.info("Sent heartbeat message") |
84 | 60 |
|
85 | 61 |
|
86 | | -async def main(server: str, port: int, apikey: str): |
| 62 | +async def main(server: str, port: int, apikey: str) -> None: |
| 63 | + """Connect to the FIX endpoint and submit a sample order.""" |
87 | 64 | global seqnum |
88 | 65 |
|
89 | | - # |
90 | | - # Trade test values |
91 | | - # N.B. Not designed for PRODUCTION trading |
92 | | - # |
93 | | - RESP_SENDER = "PT-OE" |
94 | | - SYMBOL: str = "ETH-USD" |
95 | | - PRICE: float = 2508.08 #3090.00 + randint(1, 8) |
96 | | - QUANTITY: float = .1 |
97 | | - |
98 | | - # Define server address w/ port |
99 | | - server_addr = f"{server}:{port}" |
100 | | - logger.info(f"server: {server_addr}") |
101 | | - |
102 | | - # Create context for the TLS connection |
103 | | - context = ssl.create_default_context() |
104 | | - |
105 | | - # Wrap the socket with SSL |
106 | | - context.load_verify_locations(cafile=os.getenv("CERTFILE_LOCATION", "cert.crt")) |
107 | | - logger.info("Context created") |
| 66 | + SYMBOL = "ETH-USD" |
| 67 | + PRICE = 2508.08 |
| 68 | + QUANTITY = 0.1 |
108 | 69 |
|
109 | | - context.check_hostname = True |
110 | | - context.verify_mode = ssl.CERT_REQUIRED |
| 70 | + uri = f"wss://{server}:{port}" |
| 71 | + ssl_context = ssl.create_default_context() |
| 72 | + ssl_context.load_verify_locations(cafile=os.getenv("CERTFILE_LOCATION", "cert.crt")) |
111 | 73 |
|
112 | | - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
113 | | - |
114 | | - # wait up to X secs for receiving responses |
115 | | - logger.info( f"Assigning WAIT for Fix response messages of {os.getenv('MSG_RESPONSE_WAIT', 5)} seconds") |
116 | | - sock.settimeout(int(os.getenv("MSG_RESPONSE_WAIT", 5))) |
117 | | - |
118 | | - print(f"connecting to {server} on port {port} ...") |
119 | | - sock.connect((server, port)) |
120 | | - |
121 | | - conn = context.wrap_socket(sock, server_hostname=server) |
122 | | - |
123 | | - try: |
124 | | - print("Handshaking Fix SSL/TLS connection ...") |
125 | | - conn.do_handshake() |
126 | | - |
127 | | - # Check Fix API connection with Logon message |
| 74 | + async with connect(uri, ssl=ssl_context) as ws: |
128 | 75 | msg = getMsgLogon(apikey) |
129 | | - error_msg = "" |
130 | | - try: |
131 | | - print(f"Sending Logon request {msg} to server {server} ...") |
132 | | - logger.debug(f"Sending Logon msg {msg} to server {server} ...") |
133 | | - # send Fix Logon message |
134 | | - conn.sendall(msg) |
135 | | - |
136 | | - print(f"Reading Logon response from server {server} ...") |
137 | | - logger.debug(f"Reading Logon response from server {server} ...") |
138 | | - response = conn.recv(1024) |
| 76 | + await ws.send(msg) |
| 77 | + response = await ws.recv() |
| 78 | + valid, error_msg = checkLogonMsg(response) |
| 79 | + if not valid: |
| 80 | + logger.error(f"Invalid Logon response: {error_msg}") |
| 81 | + return |
139 | 82 |
|
140 | | - print(f"Checking Logon response from server {response} ...") |
141 | | - valid, error_msg = checkLogonMsg(response) |
142 | | - if valid: |
143 | | - logger.info("Received valid Logon response") |
144 | | - # Start heartbeat thread |
145 | | - threading.Thread( |
146 | | - target=heartbeat_thread, |
147 | | - args=( |
148 | | - apikey, |
149 | | - conn, |
150 | | - stop_event, |
151 | | - ), |
152 | | - ).start() |
153 | | - else: |
154 | | - logger.error(f"Invalid Logon response: error was '{error_msg}'") |
155 | | - sys.exit(1) |
| 83 | + heartbeat_task = asyncio.create_task(send_heartbeat(ws, apikey)) |
156 | 84 |
|
157 | | - clOrdID, msg = getMsgNewOrder(SYMBOL, PRICE, QUANTITY, apikey, seqnum) |
158 | | - decoded_msg = msg.decode("utf-8") |
159 | | - print( |
160 | | - "Sending new order [%s] with order details: {%s}" |
161 | | - % (clOrdID, decoded_msg) |
162 | | - ) |
163 | | - logger.debug( |
164 | | - "Sending new order [%s] with order details: {%s}" |
165 | | - % (clOrdID, decoded_msg) |
166 | | - ) |
167 | | - conn.sendall(msg) |
| 85 | + clOrdID, order_msg = getMsgNewOrder(SYMBOL, PRICE, QUANTITY, apikey, seqnum) |
| 86 | + await ws.send(order_msg) |
| 87 | + logger.info("Sent new order") |
168 | 88 |
|
169 | | - print("Reading New Order response from server ...") |
170 | | - response = conn.recv(1024) |
| 89 | + resp = await ws.recv() |
| 90 | + logger.info(f"Order response: {resp}") |
171 | 91 |
|
172 | | - logger.debug(f"Received(decoded): {response.decode('utf-8')}") |
173 | | - valid = checkMsg(response, RESP_SENDER, apikey) |
174 | | - ( |
175 | | - print("Received valid New Order response") |
176 | | - if valid |
177 | | - else print(f"Received invalid New Order response -> {response}") |
178 | | - ) |
| 92 | + cancel_id = 11111 |
| 93 | + seqnum += 1 |
| 94 | + _, cancel_msg = getMsgCancel(clOrdID, cancel_id, SYMBOL, apikey, seqnum) |
| 95 | + await ws.send(cancel_msg) |
| 96 | + logger.info("Sent cancel request") |
179 | 97 |
|
180 | | - # |
181 | | - # iterate few times with sleep to allow trading messages from Limit Order to arrive |
182 | | - # |
183 | | - count = 0 |
184 | | - POLL_SLEEP = int( |
185 | | - os.getenv("POLL_SLEEP", 5) |
186 | | - ) # seconds to sleep between iterations |
187 | | - POLL_LIMIT = int(os.getenv("POLL_LIMIT", 10)) # iteration count |
| 98 | + await asyncio.sleep(int(os.getenv("FINAL_SLEEP", 20))) |
| 99 | + heartbeat_task.cancel() |
188 | 100 |
|
189 | | - logger.info( |
190 | | - f"Waiting for New Order [{clOrdID}] confirmation response from server [{count}] ..." |
191 | | - ) |
192 | | - |
193 | | - while count < POLL_LIMIT: |
194 | | - time.sleep(POLL_SLEEP) |
195 | | - try: |
196 | | - logger.info("Waiting for new message ...") |
197 | | - response = conn.recv(1024) |
198 | | - # response = await asyncio.get_event_loop().sock_recv(conn, 1024) |
199 | | - msg_str = response.decode("utf-8").replace(SEPARATOR, VERTLINE) |
200 | | - if msg_str is not None: |
201 | | - logger.info(f"Received(decoded):\n {msg_str}") |
202 | | - msg_list = msg_str.split("8=FIX.4.4") |
203 | | - for i, msg in enumerate(msg_list): |
204 | | - logger.debug( |
205 | | - "Recd msg: Ord '%s' Type [%s] Sts [%s]" |
206 | | - % ( |
207 | | - get_attr(msg_str, "11"), |
208 | | - translateFix("35", get_attr(msg_str, "35")), |
209 | | - translateFix("39", get_attr(msg_str, "39")), |
210 | | - ) |
211 | | - ) |
212 | | - if ( |
213 | | - get_attr(msg, "35") == "8" |
214 | | - and translateFix("39", get_attr(msg, "39")) == "New" |
215 | | - ): |
216 | | - logger.info( |
217 | | - "Exit Wait loop for order confirmation as received order status == 'New'" |
218 | | - ) |
219 | | - count = POLL_LIMIT |
220 | | - break |
221 | | - except Exception as e: |
222 | | - logger.error("Error while waiting for new message -> %s" % e) |
223 | | - count += 1 |
224 | | - |
225 | | - # setup cancel order to remove new order added above |
226 | | - cancelOrderID = 11111 # clOrdID |
227 | | - |
228 | | - print(f"Sleep {POLL_SLEEP*5} secs before starting to Cancel orders") |
229 | | - logger.info("Sleep before starting to Cancel orders") |
230 | | - time.sleep(POLL_SLEEP * 5) |
231 | | - # |
232 | | - # Cancel Order can be done if the New Limit Order above is not filled |
233 | | - # |
234 | | - logger.debug("Building Cancel Order Message for order %s" % cancelOrderID) |
235 | | - seqnum += 1 |
236 | | - now, msg = getMsgCancel(clOrdID, cancelOrderID, SYMBOL, apikey, seqnum) |
237 | | - logger.debug( |
238 | | - "Sending Cancel Order Message %s for order %s with Seqnum {seqnum}" |
239 | | - % (msg, cancelOrderID) |
240 | | - ) |
241 | | - conn.sendall(msg) |
242 | | - |
243 | | - # |
244 | | - # Await response from order cancel message |
245 | | - # |
246 | | - count = 0 |
247 | | - POLL = True |
248 | | - while POLL and count < POLL_LIMIT: |
249 | | - time.sleep(POLL_SLEEP) |
250 | | - logger.debug("Awaiting Cancel order response from server ...") |
251 | | - response = conn.recv(1024) |
252 | | - msg = response.decode("utf-8").replace(SEPARATOR, VERTLINE) |
253 | | - logger.debug( |
254 | | - "Received msg from server with type [%s] status [%s]" |
255 | | - % ( |
256 | | - translateFix("35", get_attr(msg, "35")), |
257 | | - translateFix("39", get_attr(msg, "39")), |
258 | | - ) |
259 | | - ) |
260 | | - |
261 | | - # |
262 | | - # was received message a 'heartbeat' [Msg Type = '0'] |
263 | | - # |
264 | | - if get_attr(msg, "35") == "0": |
265 | | - logger.info("Heartbeat msg received ...") |
266 | | - # |
267 | | - # received message an 'execution report' [Msg Type = '8'] |
268 | | - # |
269 | | - elif ( |
270 | | - get_attr(msg, "35") == "8" |
271 | | - and translateFix("39", get_attr(msg, "39")) == "Cancelled" |
272 | | - ): |
273 | | - logger.info( |
274 | | - "Received Order Cancel response with order status == 'Cancelled'" |
275 | | - ) |
276 | | - POLL = False |
277 | | - # |
278 | | - # Check status of the order i.e. '2' for Filled, '8' for Rejected |
279 | | - elif ( |
280 | | - get_attr(msg, "35") == "9" |
281 | | - and translateFix("39", get_attr(msg, "39")) == "Rejected" |
282 | | - ): |
283 | | - logger.info( |
284 | | - "Received Order Cancel response with order status == 'Rejected'" |
285 | | - ) |
286 | | - POLL = False |
287 | | - else: |
288 | | - logger.debug( |
289 | | - f"Received(decoded): {response.decode('utf-8').replace(SEPARATOR,VERTLINE)}" |
290 | | - ) |
291 | | - logger.debug( |
292 | | - "Recd msg with type [%s] status [%s] for order %s" |
293 | | - % ( |
294 | | - translateFix("35", get_attr(msg, "35")), |
295 | | - translateFix("39", get_attr(msg, "39")), |
296 | | - cancelOrderID, |
297 | | - ) |
298 | | - ) |
299 | | - count += 1 |
300 | | - except socket.timeout: |
301 | | - wait_time = os.getenv("MSG_RESPONSE_WAIT", 5) |
302 | | - logger.info(f"Receive operation timed out after {wait_time} seconds.") |
303 | | - except Exception as e: |
304 | | - logger.error(f"Error while processing send/receive Fix messages: {e}") |
305 | | - |
306 | | - except Exception as e: |
307 | | - logger.error(f"Failed to make Fix connection and send Order message: {e}") |
308 | | - finally: |
309 | | - # |
310 | | - # Allow 'FINAL_SLEEP' seconds to pass so we can check account balance / possition changes / open orders before closing connection which will remove open orders |
311 | | - # |
312 | | - FINAL_SLEEP = int(os.getenv("FINAL_SLEEP", 20)) |
313 | | - logger.info(f"\nWaiting {FINAL_SLEEP} secs to close connection") |
314 | | - stop_event.set() # Signal the heartbeat thread to stop |
315 | | - time.sleep(FINAL_SLEEP) |
316 | | - sock.close() |
317 | | - conn.close() |
318 | | - logger.info( |
319 | | - "\n**************************************************************************\n" |
320 | | - ) |
321 | 101 |
|
322 | 102 |
|
323 | 103 | if __name__ == "__main__": |
|
0 commit comments