-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
151 lines (126 loc) · 5.82 KB
/
main.py
File metadata and controls
151 lines (126 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from collections import defaultdict, deque
from datetime import datetime,timezone
from uuid import uuid4
import asyncio
from logger import logger # <-- Logging import
class Trade:
def __init__(self, symbol, price, quantity, maker_order_id, taker_order_id, aggressor_side, maker_fee=0.05, taker_fee=0.1):
self.timestamp = datetime.now(timezone.utc).isoformat() + "Z"
self.trade_id = str(uuid4())
self.symbol = symbol
self.price = price
self.quantity = quantity
self.maker_order_id = maker_order_id
self.taker_order_id = taker_order_id
self.aggressor_side = aggressor_side
class Order:
def __init__(self, symbol, order_type, side, quantity, price=None):
self.id = str(uuid4())
self.symbol = symbol
self.order_type = order_type
self.side = side
self.quantity = quantity
self.price = price
self.timestamp = datetime.now(timezone.utc)
class MatchingEngine:
def __init__(self):
self.books = defaultdict(lambda: {'buy': defaultdict(deque), 'sell': defaultdict(deque)})
self.trade_queue = asyncio.Queue()
async def process(self, order):
symbol = order.symbol
book = self.books[symbol]
trades = []
logger.info(f"Processing {order.order_type.upper()} order: {order.id} {order.side.upper()} {order.quantity} {order.symbol}")
if order.order_type == "market":
trades = self.match_market(order, book)
elif order.order_type in ["limit", "ioc", "fok"]:
trades = self.match_limit(order, book)
return trades
def match_market(self, order, book):
logger.info(f"Matching market order {order.id} ({order.side})")
trades = []
side = order.side
opposite = "sell" if side == "buy" else "buy"
price_levels = sorted(book[opposite].keys())
if side == "sell":
price_levels = reversed(price_levels)
for price in price_levels:
while order.quantity > 0 and book[opposite][price]:
match_order = book[opposite][price][0]
traded_qty = min(order.quantity, match_order.quantity)
trade = Trade(order.symbol, price, traded_qty, match_order.id, order.id, order.side)
trades.append(trade)
logger.info(f"TRADE: {trade.symbol} | Price: {trade.price} | Qty: {trade.quantity} | Maker: {trade.maker_order_id} | Taker: {trade.taker_order_id}")
order.quantity -= traded_qty
match_order.quantity -= traded_qty
if match_order.quantity == 0:
book[opposite][price].popleft()
if order.quantity == 0:
break
return trades
def match_limit(self, order, book):
logger.info(f"Matching limit order {order.id} ({order.side}) at price {order.price}")
trades = []
temp_trades = []
remaining_qty = order.quantity
side = order.side
opposite = "sell" if side == "buy" else "buy"
prices = sorted(book[opposite].keys())
if side == "buy":
prices = [p for p in prices if p <= order.price]
else:
prices = sorted([p for p in prices if p >= order.price], reverse=True)
for price in prices:
for match_order in list(book[opposite][price]):
if remaining_qty == 0:
break
traded_qty = min(remaining_qty, match_order.quantity)
temp_trades.append((price, traded_qty, match_order))
remaining_qty -= traded_qty
if remaining_qty == 0:
break
if remaining_qty == 0:
break
if order.order_type == "fok":
if remaining_qty > 0:
logger.info(f"FOK order {order.id} could not be fully filled. Rejecting.")
return [] # Reject entire order
# Execute trades
trades = []
for price, traded_qty, match_order in temp_trades:
trade = Trade(order.symbol, price, traded_qty, match_order.id, order.id, order.side)
trades.append(trade)
logger.info(f"TRADE: {trade.symbol} | Price: {trade.price} | Qty: {trade.quantity} | Maker: {trade.maker_order_id} | Taker: {trade.taker_order_id}")
match_order.quantity -= traded_qty
if match_order.quantity == 0:
book[opposite][price].remove(match_order)
if order.order_type == "limit" and remaining_qty > 0:
order.quantity = remaining_qty
book[side][order.price].append(order)
return trades
def best_bid_ask(self, symbol):
book = self.books[symbol]
best_bid = max(book['buy'].keys(), default=None)
best_ask = min(book['sell'].keys(), default=None)
return best_bid, best_ask
def get_open_orders(self, symbol):
book = self.books[symbol]
all_orders = []
for side in ['buy', 'sell']:
for price_level in book[side].values():
all_orders.extend(price_level)
return all_orders
def cancel_order(self, order_id):
for symbol in self.books:
for side in ['buy', 'sell']:
for price in self.books[symbol][side]:
queue = self.books[symbol][side][price]
for order in list(queue):
if order.id == order_id:
queue.remove(order)
logger.info(f"Cancelled order: {order_id}")
return True
logger.warning(f"Cancel failed: Order {order_id} not found.")
return False
# Global instance
engine = MatchingEngine()