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.
arthur_sdk/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ Arthur SDK - Simple trading for AI agents on Arthur DEX.
3
+
4
+ Trade in 3 lines:
5
+ from arthur_sdk import Arthur
6
+ client = Arthur.from_credentials_file("creds.json")
7
+ client.buy("ETH", usd=100)
8
+
9
+ Learn more: https://arthurdex.com
10
+ """
11
+
12
+ from .client import Arthur, Position, Order
13
+ from .strategies import StrategyRunner, StrategyConfig, Signal, run_strategy
14
+ from .exceptions import ArthurError, AuthError, OrderError, InsufficientFundsError
15
+ from .market_maker import MarketMaker
16
+
17
+ __version__ = "0.2.1"
18
+ __all__ = [
19
+ # Core
20
+ "Arthur",
21
+ "Position",
22
+ "Order",
23
+ # Strategies
24
+ "StrategyRunner",
25
+ "StrategyConfig",
26
+ "Signal",
27
+ "run_strategy",
28
+ # Market Making
29
+ "MarketMaker",
30
+ # Exceptions
31
+ "ArthurError",
32
+ "AuthError",
33
+ "OrderError",
34
+ "InsufficientFundsError",
35
+ ]
arthur_sdk/auth.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ Arthur SDK Authentication - ED25519 signing for Orderly API.
3
+ """
4
+
5
+ import base64
6
+ import time
7
+ from typing import Dict, Tuple
8
+
9
+ # Base58 alphabet for key decoding
10
+ BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
11
+
12
+
13
+ def base58_decode(s: str) -> bytes:
14
+ """Decode base58 string to bytes"""
15
+ num = 0
16
+ for char in s:
17
+ num = num * 58 + BASE58_ALPHABET.index(char)
18
+
19
+ # Convert to bytes
20
+ result = []
21
+ while num > 0:
22
+ result.append(num & 0xff)
23
+ num >>= 8
24
+
25
+ # Add leading zeros
26
+ for char in s:
27
+ if char == "1":
28
+ result.append(0)
29
+ else:
30
+ break
31
+
32
+ return bytes(reversed(result))
33
+
34
+
35
+ def parse_orderly_key(key: str) -> bytes:
36
+ """
37
+ Parse an Orderly key in ed25519:xxx format.
38
+
39
+ Args:
40
+ key: Key in format "ed25519:base58_encoded_key"
41
+
42
+ Returns:
43
+ Raw key bytes
44
+ """
45
+ if key.startswith("ed25519:"):
46
+ key = key[8:]
47
+ return base58_decode(key)
48
+
49
+
50
+ def sign_message(secret_key: str, message: str) -> str:
51
+ """
52
+ Sign a message with ED25519 key.
53
+
54
+ Args:
55
+ secret_key: Orderly secret key (ed25519:xxx format)
56
+ message: Message to sign
57
+
58
+ Returns:
59
+ Base64-encoded signature
60
+ """
61
+ try:
62
+ import nacl.signing
63
+
64
+ # Parse the secret key
65
+ key_bytes = parse_orderly_key(secret_key)
66
+
67
+ # Create signing key (first 32 bytes of secret key)
68
+ signing_key = nacl.signing.SigningKey(key_bytes[:32])
69
+
70
+ # Sign the message
71
+ signed = signing_key.sign(message.encode())
72
+
73
+ # Return base64-encoded signature (first 64 bytes)
74
+ return base64.b64encode(signed.signature).decode()
75
+
76
+ except ImportError:
77
+ raise ImportError(
78
+ "pynacl is required for signing. Install with: pip install pynacl"
79
+ )
80
+
81
+
82
+ def generate_auth_headers(
83
+ api_key: str,
84
+ secret_key: str,
85
+ account_id: str,
86
+ method: str,
87
+ path: str,
88
+ body: str = "",
89
+ ) -> Dict[str, str]:
90
+ """
91
+ Generate authenticated headers for Orderly API request.
92
+
93
+ Args:
94
+ api_key: Orderly API key
95
+ secret_key: Orderly secret key
96
+ account_id: Account ID
97
+ method: HTTP method
98
+ path: API path
99
+ body: Request body (for POST/PUT)
100
+
101
+ Returns:
102
+ Headers dict
103
+ """
104
+ timestamp = int(time.time() * 1000)
105
+
106
+ # Build message to sign
107
+ message = f"{timestamp}{method.upper()}{path}{body}"
108
+
109
+ # Sign message
110
+ signature = sign_message(secret_key, message)
111
+
112
+ # Note: Content-Type is NOT included here - let the caller decide
113
+ # based on the HTTP method (DELETE requests don't accept JSON)
114
+ return {
115
+ "orderly-timestamp": str(timestamp),
116
+ "orderly-account-id": account_id,
117
+ "orderly-key": api_key,
118
+ "orderly-signature": signature,
119
+ }
120
+
121
+
122
+ def verify_credentials(api_key: str, secret_key: str, account_id: str) -> bool:
123
+ """
124
+ Verify that credentials are valid format.
125
+
126
+ Args:
127
+ api_key: Orderly API key
128
+ secret_key: Orderly secret key
129
+ account_id: Account ID
130
+
131
+ Returns:
132
+ True if format is valid
133
+ """
134
+ try:
135
+ # Check key format
136
+ if not api_key.startswith("ed25519:"):
137
+ return False
138
+ if not secret_key.startswith("ed25519:"):
139
+ return False
140
+ if not account_id.startswith("0x"):
141
+ return False
142
+
143
+ # Try to parse keys
144
+ parse_orderly_key(api_key)
145
+ parse_orderly_key(secret_key)
146
+
147
+ return True
148
+ except Exception:
149
+ return False
arthur_sdk/cli.py ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Arthur CLI - Trade from command line.
4
+
5
+ Usage:
6
+ arthur run strategies/unlockoor.json --credentials creds.json
7
+ arthur status --credentials creds.json
8
+ arthur price BTC ETH SOL
9
+ arthur trade buy ETH --usd 100
10
+
11
+ Built for Arthur DEX: https://arthurdex.com
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from .client import Arthur
20
+ from .strategies import StrategyRunner, StrategyConfig
21
+
22
+
23
+ def cmd_run(args):
24
+ """Run a trading strategy"""
25
+ # Load client
26
+ client = Arthur.from_credentials_file(args.credentials)
27
+
28
+ # Create runner
29
+ runner = StrategyRunner(
30
+ client,
31
+ dry_run=args.dry_run,
32
+ on_signal=lambda s: print(f"Signal: {s.action} {s.symbol} - {s.reason}"),
33
+ on_trade=lambda t: print(f"Trade: {t}"),
34
+ )
35
+
36
+ # Run strategy
37
+ result = runner.run(args.strategy, force=True)
38
+
39
+ if args.json:
40
+ print(json.dumps(result, indent=2))
41
+ else:
42
+ print(f"Strategy: {result['strategy']}")
43
+ if result.get('signals'):
44
+ for sig in result['signals']:
45
+ print(f"Signal: {sig['action']} {sig['symbol']} - {sig['reason']}")
46
+ if result.get('trades'):
47
+ for trade in result['trades']:
48
+ print(f"Trade: {trade['status']} - {trade.get('order_id', 'N/A')}")
49
+ if result.get('errors'):
50
+ for err in result['errors']:
51
+ print(f"Error: {err}")
52
+
53
+
54
+ def cmd_status(args):
55
+ """Show account status"""
56
+ client = Arthur.from_credentials_file(args.credentials)
57
+
58
+ summary = client.summary()
59
+
60
+ if args.json:
61
+ print(json.dumps(summary, indent=2))
62
+ else:
63
+ print(f"Balance: ${summary['balance']:,.2f}")
64
+ print(f"Equity: ${summary['equity']:,.2f}")
65
+ print(f"Unrealized PnL: ${summary['unrealized_pnl']:,.2f}")
66
+ print(f"Open Positions: {summary['positions']}")
67
+
68
+ if summary['position_details']:
69
+ print("\nPositions:")
70
+ for pos in summary['position_details']:
71
+ pnl_sign = "+" if pos['pnl'] >= 0 else ""
72
+ print(f" {pos['symbol']}: {pos['side']} {pos['size']:.4f}")
73
+ print(f" Entry: ${pos['entry']:,.2f} → ${pos['mark']:,.2f}")
74
+ print(f" PnL: {pnl_sign}${pos['pnl']:,.2f} ({pnl_sign}{pos['pnl_pct']:.1f}%)")
75
+
76
+
77
+ def cmd_price(args):
78
+ """Get current prices"""
79
+ client = Arthur() # No auth needed for prices
80
+
81
+ prices = {}
82
+ for symbol in args.symbols:
83
+ try:
84
+ prices[symbol] = client.price(symbol)
85
+ except Exception as e:
86
+ prices[symbol] = f"Error: {e}"
87
+
88
+ if args.json:
89
+ print(json.dumps(prices, indent=2))
90
+ else:
91
+ for symbol, price in prices.items():
92
+ if isinstance(price, float):
93
+ print(f"{symbol}: ${price:,.2f}")
94
+ else:
95
+ print(f"{symbol}: {price}")
96
+
97
+
98
+ def cmd_trade(args):
99
+ """Execute a trade"""
100
+ client = Arthur.from_credentials_file(args.credentials)
101
+
102
+ if args.dry_run:
103
+ print(f"[DRY RUN] Would {args.action} {args.symbol}")
104
+ price = client.price(args.symbol)
105
+ if args.usd:
106
+ size = args.usd / price
107
+ print(f" Size: {size:.6f} @ ${price:,.2f} = ${args.usd:.2f}")
108
+ elif args.size:
109
+ print(f" Size: {args.size} @ ${price:,.2f} = ${args.size * price:.2f}")
110
+ return
111
+
112
+ try:
113
+ if args.action == "buy":
114
+ order = client.buy(args.symbol, size=args.size, usd=args.usd)
115
+ elif args.action == "sell":
116
+ order = client.sell(args.symbol, size=args.size, usd=args.usd)
117
+ elif args.action == "close":
118
+ order = client.close(args.symbol, size=args.size)
119
+
120
+ if order:
121
+ print(f"✅ Order {order.order_id}")
122
+ print(f" {order.side} {order.size:.6f} {order.symbol}")
123
+ print(f" Status: {order.status}")
124
+ else:
125
+ print("No order placed (no position to close?)")
126
+
127
+ except Exception as e:
128
+ print(f"❌ Error: {e}")
129
+ sys.exit(1)
130
+
131
+
132
+ def main():
133
+ parser = argparse.ArgumentParser(
134
+ description="Arthur SDK - Trade on Arthur DEX from command line"
135
+ )
136
+ parser.add_argument("--version", action="version", version="arthur-sdk 0.2.0")
137
+
138
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
139
+
140
+ # Run command
141
+ run_parser = subparsers.add_parser("run", help="Run a trading strategy")
142
+ run_parser.add_argument("strategy", help="Path to strategy JSON file")
143
+ run_parser.add_argument("-c", "--credentials", default="~/.config/arthur/credentials.json",
144
+ help="Path to credentials file")
145
+ run_parser.add_argument("--dry-run", action="store_true", help="Don't execute trades")
146
+ run_parser.add_argument("--json", action="store_true", help="Output as JSON")
147
+ run_parser.set_defaults(func=cmd_run)
148
+
149
+ # Status command
150
+ status_parser = subparsers.add_parser("status", help="Show account status")
151
+ status_parser.add_argument("-c", "--credentials", default="~/.config/arthur/credentials.json",
152
+ help="Path to credentials file")
153
+ status_parser.add_argument("--json", action="store_true", help="Output as JSON")
154
+ status_parser.set_defaults(func=cmd_status)
155
+
156
+ # Price command
157
+ price_parser = subparsers.add_parser("price", help="Get current prices")
158
+ price_parser.add_argument("symbols", nargs="+", help="Symbols to check (e.g., BTC ETH)")
159
+ price_parser.add_argument("--json", action="store_true", help="Output as JSON")
160
+ price_parser.set_defaults(func=cmd_price)
161
+
162
+ # Trade command
163
+ trade_parser = subparsers.add_parser("trade", help="Execute a trade")
164
+ trade_parser.add_argument("action", choices=["buy", "sell", "close"], help="Trade action")
165
+ trade_parser.add_argument("symbol", help="Symbol to trade (e.g., ETH)")
166
+ trade_parser.add_argument("--size", type=float, help="Position size")
167
+ trade_parser.add_argument("--usd", type=float, help="Position size in USD")
168
+ trade_parser.add_argument("-c", "--credentials", default="~/.config/arthur/credentials.json",
169
+ help="Path to credentials file")
170
+ trade_parser.add_argument("--dry-run", action="store_true", help="Don't execute trade")
171
+ trade_parser.set_defaults(func=cmd_trade)
172
+
173
+ args = parser.parse_args()
174
+
175
+ if not args.command:
176
+ parser.print_help()
177
+ sys.exit(1)
178
+
179
+ args.func(args)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()