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 +35 -0
- arthur_sdk/auth.py +149 -0
- arthur_sdk/cli.py +183 -0
- arthur_sdk/client.py +1075 -0
- arthur_sdk/exceptions.py +31 -0
- arthur_sdk/market_maker.py +326 -0
- arthur_sdk/strategies.py +576 -0
- arthur_sdk-0.2.1.dist-info/METADATA +225 -0
- arthur_sdk-0.2.1.dist-info/RECORD +13 -0
- arthur_sdk-0.2.1.dist-info/WHEEL +5 -0
- arthur_sdk-0.2.1.dist-info/entry_points.txt +2 -0
- arthur_sdk-0.2.1.dist-info/licenses/LICENSE +21 -0
- arthur_sdk-0.2.1.dist-info/top_level.txt +1 -0
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()
|