arthur-sdk 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ """Arthur SDK Exceptions"""
2
+
3
+
4
+ class ArthurError(Exception):
5
+ """Base exception for Arthur SDK"""
6
+ pass
7
+
8
+
9
+ class AuthError(ArthurError):
10
+ """Authentication failed"""
11
+ pass
12
+
13
+
14
+ class OrderError(ArthurError):
15
+ """Order placement failed"""
16
+ pass
17
+
18
+
19
+ class InsufficientFundsError(ArthurError):
20
+ """Not enough balance for operation"""
21
+ pass
22
+
23
+
24
+ class PositionError(ArthurError):
25
+ """Position operation failed"""
26
+ pass
27
+
28
+
29
+ class RateLimitError(ArthurError):
30
+ """Rate limit exceeded"""
31
+ pass
@@ -0,0 +1,326 @@
1
+ """
2
+ Market Maker Strategy Runner for Orderly Network.
3
+
4
+ Simple market making: place limit orders on both sides, manage inventory,
5
+ capture spread.
6
+ """
7
+
8
+ import json
9
+ import time
10
+ from dataclasses import dataclass
11
+ from typing import Optional, Dict, Any
12
+ from pathlib import Path
13
+
14
+ from .client import Arthur, Order, Position
15
+ from .exceptions import ArthurError, OrderError
16
+
17
+
18
+ @dataclass
19
+ class MMConfig:
20
+ """Market maker configuration"""
21
+ name: str
22
+ symbol: str
23
+
24
+ # Spread settings
25
+ base_spread_bps: float = 30 # Base spread in basis points
26
+ min_spread_bps: float = 15 # Minimum spread
27
+
28
+ # Size settings
29
+ order_size_usd: float = 50 # Size per side in USD
30
+ max_inventory_usd: float = 300 # Max inventory before skewing
31
+ levels: int = 1 # Number of price levels
32
+
33
+ # Skew settings
34
+ skew_per_100_usd: float = 5 # BPS to skew per $100 inventory
35
+
36
+ # Risk settings
37
+ max_position_usd: float = 500
38
+ stop_loss_pct: float = 5
39
+ daily_loss_limit_usd: float = 50
40
+
41
+ # Execution
42
+ post_only: bool = True
43
+ requote_interval_sec: float = 30
44
+ min_edge_bps: float = 5
45
+
46
+ # Flags
47
+ dry_run: bool = True
48
+ log_quotes: bool = True
49
+
50
+ @classmethod
51
+ def from_file(cls, path: str) -> "MMConfig":
52
+ """Load config from JSON file"""
53
+ with open(Path(path).expanduser()) as f:
54
+ data = json.load(f)
55
+
56
+ mm = data.get("market_making", {})
57
+ risk = data.get("risk", {})
58
+ exec_cfg = data.get("execution", {})
59
+ flags = data.get("flags", {})
60
+
61
+ return cls(
62
+ name=data.get("name", "Unnamed MM"),
63
+ symbol=data.get("symbol", "ORDER"),
64
+ base_spread_bps=mm.get("base_spread_bps", 30),
65
+ min_spread_bps=mm.get("min_spread_bps", 15),
66
+ order_size_usd=mm.get("order_size_usd", 50),
67
+ max_inventory_usd=mm.get("max_inventory_usd", 300),
68
+ levels=mm.get("levels", 1),
69
+ skew_per_100_usd=mm.get("skew_per_100_usd", 5),
70
+ max_position_usd=risk.get("max_position_usd", 500),
71
+ stop_loss_pct=risk.get("stop_loss_pct", 5),
72
+ daily_loss_limit_usd=risk.get("daily_loss_limit_usd", 50),
73
+ post_only=exec_cfg.get("post_only", True),
74
+ requote_interval_sec=mm.get("requote_interval_sec", 30),
75
+ min_edge_bps=exec_cfg.get("min_edge_bps", 5),
76
+ dry_run=flags.get("dry_run", True),
77
+ log_quotes=flags.get("log_quotes", True),
78
+ )
79
+
80
+
81
+ class MarketMaker:
82
+ """
83
+ Simple market maker for Orderly Network.
84
+
85
+ Strategy:
86
+ 1. Get mid price from orderbook
87
+ 2. Calculate bid/ask prices based on spread
88
+ 3. Skew quotes based on inventory
89
+ 4. Place/update limit orders
90
+ 5. Repeat every N seconds
91
+
92
+ Example:
93
+ client = Arthur.from_credentials_file("creds.json")
94
+ mm = MarketMaker(client, MMConfig.from_file("strategies/order-mm.json"))
95
+ mm.run_once() # Single quote cycle
96
+ mm.run_loop() # Continuous quoting
97
+ """
98
+
99
+ def __init__(self, client: Arthur, config: MMConfig):
100
+ self.client = client
101
+ self.config = config
102
+ self.symbol = client._normalize_symbol(config.symbol)
103
+
104
+ # State
105
+ self.active_orders: Dict[str, Order] = {} # order_id -> Order
106
+ self.last_quote_time: float = 0
107
+ self.daily_pnl: float = 0
108
+ self.session_start: float = time.time()
109
+
110
+ def run_once(self) -> Dict[str, Any]:
111
+ """
112
+ Run one quote cycle.
113
+
114
+ Returns:
115
+ Dict with quote details and status
116
+ """
117
+ result = {
118
+ "timestamp": int(time.time() * 1000),
119
+ "symbol": self.symbol,
120
+ "dry_run": self.config.dry_run,
121
+ }
122
+
123
+ try:
124
+ # 1. Get market data
125
+ spread_info = self.client.spread(self.config.symbol)
126
+ mid_price = spread_info["mid"]
127
+ market_spread_bps = spread_info["spread_bps"]
128
+
129
+ result["mid_price"] = mid_price
130
+ result["market_spread_bps"] = market_spread_bps
131
+
132
+ # 2. Get current position
133
+ position = self.client.position(self.config.symbol)
134
+ inventory_usd = 0
135
+ if position:
136
+ inventory_usd = position.size * mid_price
137
+ if position.side == "SHORT":
138
+ inventory_usd = -inventory_usd
139
+
140
+ result["inventory_usd"] = inventory_usd
141
+
142
+ # 3. Check risk limits
143
+ if abs(inventory_usd) >= self.config.max_position_usd:
144
+ result["status"] = "max_inventory"
145
+ result["action"] = "cancel_all"
146
+ if not self.config.dry_run:
147
+ self.client.cancel_all(self.symbol)
148
+ return result
149
+
150
+ # 4. Calculate quotes
151
+ quotes = self._calculate_quotes(mid_price, inventory_usd)
152
+ result["quotes"] = quotes
153
+
154
+ # 5. Place orders
155
+ if self.config.dry_run:
156
+ result["status"] = "dry_run"
157
+ result["action"] = "would_quote"
158
+ else:
159
+ # Cancel existing orders
160
+ self.client.cancel_all(self.symbol)
161
+
162
+ # Place new quotes
163
+ orders = self.client.quote(
164
+ symbol=self.config.symbol,
165
+ bid_price=quotes["bid_price"],
166
+ ask_price=quotes["ask_price"],
167
+ size=quotes["size"],
168
+ cancel_existing=False, # Already cancelled
169
+ )
170
+
171
+ result["orders"] = {
172
+ "bid": orders["bid"].order_id,
173
+ "ask": orders["ask"].order_id,
174
+ }
175
+ result["status"] = "quoted"
176
+ result["action"] = "placed_orders"
177
+
178
+ self.last_quote_time = time.time()
179
+
180
+ if self.config.log_quotes:
181
+ self._log_quote(result)
182
+
183
+ except Exception as e:
184
+ result["status"] = "error"
185
+ result["error"] = str(e)
186
+
187
+ return result
188
+
189
+ def _calculate_quotes(self, mid_price: float, inventory_usd: float) -> Dict:
190
+ """Calculate bid and ask prices based on config and inventory"""
191
+
192
+ # Base spread
193
+ spread_bps = self.config.base_spread_bps
194
+
195
+ # Inventory skew: widen on the side we're heavy
196
+ skew_bps = (inventory_usd / 100) * self.config.skew_per_100_usd
197
+
198
+ # Calculate prices
199
+ half_spread = (spread_bps / 10000) * mid_price / 2
200
+ skew_amount = (skew_bps / 10000) * mid_price
201
+
202
+ bid_price = mid_price - half_spread - skew_amount
203
+ ask_price = mid_price + half_spread - skew_amount
204
+
205
+ # Ensure minimum spread
206
+ min_spread = (self.config.min_spread_bps / 10000) * mid_price
207
+ if ask_price - bid_price < min_spread:
208
+ # Widen symmetrically
209
+ gap = min_spread - (ask_price - bid_price)
210
+ bid_price -= gap / 2
211
+ ask_price += gap / 2
212
+
213
+ # Calculate size
214
+ size = self.config.order_size_usd / mid_price
215
+
216
+ # Round to tick size (ORDER has base_tick=1)
217
+ size = max(1, round(size))
218
+
219
+ # Round prices to quote tick (0.00001)
220
+ bid_price = round(bid_price, 5)
221
+ ask_price = round(ask_price, 5)
222
+
223
+ return {
224
+ "bid_price": bid_price,
225
+ "ask_price": ask_price,
226
+ "size": size,
227
+ "spread_bps": ((ask_price - bid_price) / mid_price) * 10000,
228
+ "skew_bps": skew_bps,
229
+ }
230
+
231
+ def _log_quote(self, result: Dict):
232
+ """Log quote details"""
233
+ quotes = result.get("quotes", {})
234
+ mode = "[DRY]" if self.config.dry_run else "[LIVE]"
235
+
236
+ print(f"{mode} {self.config.symbol} | "
237
+ f"mid=${result.get('mid_price', 0):.5f} | "
238
+ f"bid=${quotes.get('bid_price', 0):.5f} / "
239
+ f"ask=${quotes.get('ask_price', 0):.5f} | "
240
+ f"spread={quotes.get('spread_bps', 0):.1f}bps | "
241
+ f"inv=${result.get('inventory_usd', 0):.0f}")
242
+
243
+ def run_loop(self, duration_sec: Optional[float] = None):
244
+ """
245
+ Run continuous quoting loop.
246
+
247
+ Args:
248
+ duration_sec: Run for this many seconds (None = forever)
249
+ """
250
+ print(f"Starting market maker for {self.config.symbol}")
251
+ print(f"Spread: {self.config.base_spread_bps}bps | "
252
+ f"Size: ${self.config.order_size_usd} | "
253
+ f"Interval: {self.config.requote_interval_sec}s")
254
+ print(f"Mode: {'DRY RUN' if self.config.dry_run else 'LIVE'}")
255
+ print("-" * 60)
256
+
257
+ start_time = time.time()
258
+
259
+ try:
260
+ while True:
261
+ self.run_once()
262
+
263
+ # Check duration
264
+ if duration_sec and (time.time() - start_time) >= duration_sec:
265
+ break
266
+
267
+ time.sleep(self.config.requote_interval_sec)
268
+
269
+ except KeyboardInterrupt:
270
+ print("\nStopping market maker...")
271
+ finally:
272
+ # Cancel all orders on exit
273
+ if not self.config.dry_run:
274
+ print("Cancelling all orders...")
275
+ self.client.cancel_all(self.symbol)
276
+
277
+ print("Market maker stopped.")
278
+
279
+ def status(self) -> Dict:
280
+ """Get current MM status"""
281
+ position = self.client.position(self.config.symbol)
282
+ orders = self.client.orders(self.config.symbol)
283
+ spread = self.client.spread(self.config.symbol)
284
+
285
+ return {
286
+ "symbol": self.config.symbol,
287
+ "mode": "dry_run" if self.config.dry_run else "live",
288
+ "mid_price": spread["mid"],
289
+ "market_spread_bps": spread["spread_bps"],
290
+ "position": {
291
+ "side": position.side if position else None,
292
+ "size": position.size if position else 0,
293
+ "pnl": position.unrealized_pnl if position else 0,
294
+ } if position else None,
295
+ "open_orders": len(orders),
296
+ "uptime_sec": time.time() - self.session_start,
297
+ }
298
+
299
+
300
+ def run_mm(config_path: str, credentials_path: str, duration: Optional[float] = None):
301
+ """
302
+ Run market maker from config file.
303
+
304
+ Args:
305
+ config_path: Path to strategy JSON
306
+ credentials_path: Path to credentials JSON
307
+ duration: Run duration in seconds (None = forever)
308
+ """
309
+ client = Arthur.from_credentials_file(credentials_path)
310
+ config = MMConfig.from_file(config_path)
311
+ mm = MarketMaker(client, config)
312
+ mm.run_loop(duration_sec=duration)
313
+
314
+
315
+ if __name__ == "__main__":
316
+ import sys
317
+
318
+ if len(sys.argv) < 3:
319
+ print("Usage: python -m orderly_agent.market_maker <strategy.json> <credentials.json> [duration_sec]")
320
+ sys.exit(1)
321
+
322
+ strategy = sys.argv[1]
323
+ creds = sys.argv[2]
324
+ duration = float(sys.argv[3]) if len(sys.argv) > 3 else None
325
+
326
+ run_mm(strategy, creds, duration)