melaya 0.1.0__tar.gz
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.
- melaya-0.1.0/.gitignore +32 -0
- melaya-0.1.0/PKG-INFO +136 -0
- melaya-0.1.0/README.md +113 -0
- melaya-0.1.0/e2e/smoke.py +270 -0
- melaya-0.1.0/pyproject.toml +46 -0
- melaya-0.1.0/src/melaya/__init__.py +13 -0
- melaya-0.1.0/src/melaya/account.py +27 -0
- melaya-0.1.0/src/melaya/backtest.py +70 -0
- melaya-0.1.0/src/melaya/client.py +101 -0
- melaya-0.1.0/src/melaya/errors.py +20 -0
- melaya-0.1.0/src/melaya/market.py +134 -0
- melaya-0.1.0/src/melaya/sim.py +96 -0
- melaya-0.1.0/src/melaya/strategies.py +122 -0
- melaya-0.1.0/src/melaya/stream.py +101 -0
- melaya-0.1.0/src/melaya/trade.py +167 -0
melaya-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
**/node_modules/
|
|
3
|
+
**/dist/
|
|
4
|
+
**/target/
|
|
5
|
+
**/build/
|
|
6
|
+
**/.gradle/
|
|
7
|
+
**/bin/
|
|
8
|
+
**/obj/
|
|
9
|
+
**/vendor/
|
|
10
|
+
**/__pycache__/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
# Gradle/JVM
|
|
14
|
+
*.class
|
|
15
|
+
.gradle/
|
|
16
|
+
# logs / tmp
|
|
17
|
+
*.log
|
|
18
|
+
*.tmp
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
# package locks we don't ship from staging
|
|
23
|
+
**/go.sum
|
|
24
|
+
**/*.exe
|
|
25
|
+
|
|
26
|
+
# secrets (belt-and-suspenders)
|
|
27
|
+
.env
|
|
28
|
+
.env.*
|
|
29
|
+
**/.env
|
|
30
|
+
*.pem
|
|
31
|
+
*.key
|
|
32
|
+
id_rsa*
|
melaya-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: melaya
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Melaya unified market-data & streaming API across 70+ venues.
|
|
5
|
+
Project-URL: Homepage, https://melaya.org
|
|
6
|
+
Project-URL: Documentation, https://melaya.org/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/melaya-labs/melaya
|
|
8
|
+
Project-URL: Issues, https://github.com/melaya-labs/melaya/issues
|
|
9
|
+
Author: Melaya
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: agentic-ai,crypto,exchange,market-data,melaya,ohlcv,orderbook,sdk,ticker,trading,websocket
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Provides-Extra: stream
|
|
21
|
+
Requires-Dist: websockets>=12; extra == 'stream'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# melaya (Python SDK)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for the **[Melaya](https://melaya.org)** trading platform — normalized market data, paper + live trading, backtesting, and an AI agentic trading crew across **70+ venues**, powered by an in-house Rust engine.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install melaya # REST
|
|
32
|
+
pip install "melaya[stream]" # REST + WebSocket streaming
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from melaya import Melaya
|
|
39
|
+
|
|
40
|
+
m = Melaya(api_key="mk_...") # keys are prefixed `mk_`
|
|
41
|
+
|
|
42
|
+
# Normalized ticker from any of 70+ venues
|
|
43
|
+
t = m.market.ticker(exchange="binance", symbol="BTC/USDT", market="spot")
|
|
44
|
+
print(t["last"], t["bid"], t["ask"])
|
|
45
|
+
|
|
46
|
+
# Order book + candles
|
|
47
|
+
book = m.market.orderbook(exchange="bybit", symbol="BTC/USDT", market="spot", limit=20)
|
|
48
|
+
candles = m.market.ohlcv(exchange="okx", symbol="ETH/USDT", timeframe="1h", limit=200)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Streaming (async)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from melaya import Melaya
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
m = Melaya(api_key="mk_...")
|
|
59
|
+
async for t in m.stream.ticker(exchange="binance", symbol="BTC/USDT", market="spot"):
|
|
60
|
+
print(t["last"])
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Trading
|
|
66
|
+
|
|
67
|
+
The same client covers your account, paper trading, live strategies, and backtests. Reads need only your `mk_` key; live order placement needs a connected exchange key (`m.account.keys()`).
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Account
|
|
71
|
+
keys = m.account.keys() # [{"apiKeyId": "BINANCEUSDM_0", "exchange": ..., "market": ...}]
|
|
72
|
+
usage = m.account.usage()
|
|
73
|
+
|
|
74
|
+
# Strategies — create() launches immediately. Paper (dry_run) needs no exchange key.
|
|
75
|
+
# SDK-launchable strategies are `custom` Rhai definitions (an `evaluate()` that
|
|
76
|
+
# emits signals: emit_long / emit_short / emit_close).
|
|
77
|
+
res = m.strategies.create(
|
|
78
|
+
name="My first bot", strategy_type="custom",
|
|
79
|
+
exchange="binanceusdm", symbol="BTC/USDT:USDT", market="FUTURES", dry_run=True,
|
|
80
|
+
params={"language": "rhai",
|
|
81
|
+
"definition": 'fn evaluate() { emit_long(param("qty")); }',
|
|
82
|
+
"qty": 0.001}, # dry_run=False + api_key_id places real orders
|
|
83
|
+
)
|
|
84
|
+
sid = res["strategyId"]
|
|
85
|
+
m.strategies.pause(sid)
|
|
86
|
+
m.strategies.resume(sid)
|
|
87
|
+
trades = m.strategies.trades(sid)
|
|
88
|
+
|
|
89
|
+
# Paper trading (sim broker) — synthetic fills, no venue state
|
|
90
|
+
bal = m.sim.balance(strategy_id=sid)
|
|
91
|
+
fill = m.sim.create_order(strategy_id=sid, exchange="binanceusdm",
|
|
92
|
+
symbol="BTC/USDT:USDT", side="buy", type="market",
|
|
93
|
+
amount=0.001, market="FUTURES")
|
|
94
|
+
|
|
95
|
+
# Backtest on the Rust engine
|
|
96
|
+
import time
|
|
97
|
+
start = m.backtest.start({"strategyType": "custom", "exchange": "binance",
|
|
98
|
+
"symbol": "BTC/USDT", "timeframe": "1h", "language": "rhai",
|
|
99
|
+
"definition": 'fn evaluate() { emit_long(param("qty")); }',
|
|
100
|
+
"params": {"qty": 0.001}})
|
|
101
|
+
job_id = start["job_id"]
|
|
102
|
+
while m.backtest.job(job_id)["status"] not in ("done", "error"):
|
|
103
|
+
time.sleep(2)
|
|
104
|
+
result = m.backtest.results(job_id) # metrics, equity_curve, ohlcv
|
|
105
|
+
|
|
106
|
+
# Live private strategy feed (async; ticket minted automatically)
|
|
107
|
+
async for ev in m.stream.strategies():
|
|
108
|
+
print(ev["type"], ev.get("strategyId"))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Authentication
|
|
112
|
+
|
|
113
|
+
Create an API key in the dashboard (**melaya.org → Settings → API Keys**). Keys are prefixed `mk_`; the SDK sends it on every REST call and WebSocket connection. Public market-data and account/strategy reads work with the key alone. **Live** order placement and live strategy launches additionally require a connected exchange key — connect one in **Settings → Connectors**, then reference it by `api_key_id`. Paper trading and backtesting never touch a venue and need no exchange credentials.
|
|
114
|
+
|
|
115
|
+
## API surface
|
|
116
|
+
|
|
117
|
+
| Area | Methods |
|
|
118
|
+
|---|---|
|
|
119
|
+
| Reference | `market.list_exchanges()`, `catalog_counts()` |
|
|
120
|
+
| Market data | `market.ticker`, `orderbook`, `ohlcv`, `ohlcv_multi`, `trades`, `markets`, `currencies`, `market_constraints`, `status`, `time` |
|
|
121
|
+
| Batch / derivatives | `market.tickers`, `funding_rates`, `funding_rate_history`, `funding_rate_history_multi`, `open_interest`, `open_interest_history`, `open_interest_history_multi`, `instruments`, `liquidation_events` |
|
|
122
|
+
| Prediction markets | `market.prediction_markets` (polymarket, kalshi, drift_pm, sxbet, azuro, overtime) |
|
|
123
|
+
| Account | `account.keys`, `usage`, `api_key_status` |
|
|
124
|
+
| Strategies | `strategies.create`, `list`, `get`, `pause`, `resume`, `stop`, `delete`, `update_params`, `status`, `performance`, `executions`, `trades`, `logs` |
|
|
125
|
+
| AI optimizer | `strategies.ai_opt_start`, `ai_opt_status`, `ai_opt_approve`, `ai_opt_stop`, `ai_opt_runs` |
|
|
126
|
+
| Paper trading | `sim.balance`, `positions`, `open_orders`, `my_trades`, `create_order`, `cancel_order`, `list_accounts` |
|
|
127
|
+
| Backtesting | `backtest.start`, `job`, `results`, `trades`, `sweep`, `list`, `favorites`, `funding_range`, `cancel`, `delete`, `delete_all` |
|
|
128
|
+
| Public streaming | `stream.ticker`, `orderbook`, `ohlcv`, `trades`, `liquidations` |
|
|
129
|
+
| Private streaming | `stream.strategies`, `stream.private` |
|
|
130
|
+
| Live trading | `trade.balance`, `positions`, `open_orders`, `orders`, `closed_orders`, `my_trades`, `my_trades_history`, `plan_orders`, `positions_history`, `leverage`, `leverage_tiers`, `create_order`, `cancel_order`, `amend_order`, `cancel_all_orders`, `cancel_plan_orders`, `close_position`, `set_leverage`, `set_margin_mode`, `set_position_mode` |
|
|
131
|
+
|
|
132
|
+
Full docs: **[melaya.org/docs](https://melaya.org/docs)**.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
[Apache-2.0](../../LICENSE)
|
melaya-0.1.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# melaya (Python SDK)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the **[Melaya](https://melaya.org)** trading platform — normalized market data, paper + live trading, backtesting, and an AI agentic trading crew across **70+ venues**, powered by an in-house Rust engine.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install melaya # REST
|
|
9
|
+
pip install "melaya[stream]" # REST + WebSocket streaming
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from melaya import Melaya
|
|
16
|
+
|
|
17
|
+
m = Melaya(api_key="mk_...") # keys are prefixed `mk_`
|
|
18
|
+
|
|
19
|
+
# Normalized ticker from any of 70+ venues
|
|
20
|
+
t = m.market.ticker(exchange="binance", symbol="BTC/USDT", market="spot")
|
|
21
|
+
print(t["last"], t["bid"], t["ask"])
|
|
22
|
+
|
|
23
|
+
# Order book + candles
|
|
24
|
+
book = m.market.orderbook(exchange="bybit", symbol="BTC/USDT", market="spot", limit=20)
|
|
25
|
+
candles = m.market.ohlcv(exchange="okx", symbol="ETH/USDT", timeframe="1h", limit=200)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Streaming (async)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
from melaya import Melaya
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
m = Melaya(api_key="mk_...")
|
|
36
|
+
async for t in m.stream.ticker(exchange="binance", symbol="BTC/USDT", market="spot"):
|
|
37
|
+
print(t["last"])
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Trading
|
|
43
|
+
|
|
44
|
+
The same client covers your account, paper trading, live strategies, and backtests. Reads need only your `mk_` key; live order placement needs a connected exchange key (`m.account.keys()`).
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# Account
|
|
48
|
+
keys = m.account.keys() # [{"apiKeyId": "BINANCEUSDM_0", "exchange": ..., "market": ...}]
|
|
49
|
+
usage = m.account.usage()
|
|
50
|
+
|
|
51
|
+
# Strategies — create() launches immediately. Paper (dry_run) needs no exchange key.
|
|
52
|
+
# SDK-launchable strategies are `custom` Rhai definitions (an `evaluate()` that
|
|
53
|
+
# emits signals: emit_long / emit_short / emit_close).
|
|
54
|
+
res = m.strategies.create(
|
|
55
|
+
name="My first bot", strategy_type="custom",
|
|
56
|
+
exchange="binanceusdm", symbol="BTC/USDT:USDT", market="FUTURES", dry_run=True,
|
|
57
|
+
params={"language": "rhai",
|
|
58
|
+
"definition": 'fn evaluate() { emit_long(param("qty")); }',
|
|
59
|
+
"qty": 0.001}, # dry_run=False + api_key_id places real orders
|
|
60
|
+
)
|
|
61
|
+
sid = res["strategyId"]
|
|
62
|
+
m.strategies.pause(sid)
|
|
63
|
+
m.strategies.resume(sid)
|
|
64
|
+
trades = m.strategies.trades(sid)
|
|
65
|
+
|
|
66
|
+
# Paper trading (sim broker) — synthetic fills, no venue state
|
|
67
|
+
bal = m.sim.balance(strategy_id=sid)
|
|
68
|
+
fill = m.sim.create_order(strategy_id=sid, exchange="binanceusdm",
|
|
69
|
+
symbol="BTC/USDT:USDT", side="buy", type="market",
|
|
70
|
+
amount=0.001, market="FUTURES")
|
|
71
|
+
|
|
72
|
+
# Backtest on the Rust engine
|
|
73
|
+
import time
|
|
74
|
+
start = m.backtest.start({"strategyType": "custom", "exchange": "binance",
|
|
75
|
+
"symbol": "BTC/USDT", "timeframe": "1h", "language": "rhai",
|
|
76
|
+
"definition": 'fn evaluate() { emit_long(param("qty")); }',
|
|
77
|
+
"params": {"qty": 0.001}})
|
|
78
|
+
job_id = start["job_id"]
|
|
79
|
+
while m.backtest.job(job_id)["status"] not in ("done", "error"):
|
|
80
|
+
time.sleep(2)
|
|
81
|
+
result = m.backtest.results(job_id) # metrics, equity_curve, ohlcv
|
|
82
|
+
|
|
83
|
+
# Live private strategy feed (async; ticket minted automatically)
|
|
84
|
+
async for ev in m.stream.strategies():
|
|
85
|
+
print(ev["type"], ev.get("strategyId"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Authentication
|
|
89
|
+
|
|
90
|
+
Create an API key in the dashboard (**melaya.org → Settings → API Keys**). Keys are prefixed `mk_`; the SDK sends it on every REST call and WebSocket connection. Public market-data and account/strategy reads work with the key alone. **Live** order placement and live strategy launches additionally require a connected exchange key — connect one in **Settings → Connectors**, then reference it by `api_key_id`. Paper trading and backtesting never touch a venue and need no exchange credentials.
|
|
91
|
+
|
|
92
|
+
## API surface
|
|
93
|
+
|
|
94
|
+
| Area | Methods |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Reference | `market.list_exchanges()`, `catalog_counts()` |
|
|
97
|
+
| Market data | `market.ticker`, `orderbook`, `ohlcv`, `ohlcv_multi`, `trades`, `markets`, `currencies`, `market_constraints`, `status`, `time` |
|
|
98
|
+
| Batch / derivatives | `market.tickers`, `funding_rates`, `funding_rate_history`, `funding_rate_history_multi`, `open_interest`, `open_interest_history`, `open_interest_history_multi`, `instruments`, `liquidation_events` |
|
|
99
|
+
| Prediction markets | `market.prediction_markets` (polymarket, kalshi, drift_pm, sxbet, azuro, overtime) |
|
|
100
|
+
| Account | `account.keys`, `usage`, `api_key_status` |
|
|
101
|
+
| Strategies | `strategies.create`, `list`, `get`, `pause`, `resume`, `stop`, `delete`, `update_params`, `status`, `performance`, `executions`, `trades`, `logs` |
|
|
102
|
+
| AI optimizer | `strategies.ai_opt_start`, `ai_opt_status`, `ai_opt_approve`, `ai_opt_stop`, `ai_opt_runs` |
|
|
103
|
+
| Paper trading | `sim.balance`, `positions`, `open_orders`, `my_trades`, `create_order`, `cancel_order`, `list_accounts` |
|
|
104
|
+
| Backtesting | `backtest.start`, `job`, `results`, `trades`, `sweep`, `list`, `favorites`, `funding_range`, `cancel`, `delete`, `delete_all` |
|
|
105
|
+
| Public streaming | `stream.ticker`, `orderbook`, `ohlcv`, `trades`, `liquidations` |
|
|
106
|
+
| Private streaming | `stream.strategies`, `stream.private` |
|
|
107
|
+
| Live trading | `trade.balance`, `positions`, `open_orders`, `orders`, `closed_orders`, `my_trades`, `my_trades_history`, `plan_orders`, `positions_history`, `leverage`, `leverage_tiers`, `create_order`, `cancel_order`, `amend_order`, `cancel_all_orders`, `cancel_plan_orders`, `close_position`, `set_leverage`, `set_margin_mode`, `set_position_mode` |
|
|
108
|
+
|
|
109
|
+
Full docs: **[melaya.org/docs](https://melaya.org/docs)**.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[Apache-2.0](../../LICENSE)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""FULL endpoint validation for the `melaya` Python SDK — every method, live.
|
|
2
|
+
Safety: paper/sim only. NEVER places a live order or launches a live strategy.
|
|
3
|
+
Destructive/billable endpoints (backtest.delete_all, strategies.ai_opt_start /
|
|
4
|
+
ai_opt_approve) are WIRED-checked, not invoked.
|
|
5
|
+
Run: MK=mk_... python smoke.py (SDK src on sys.path)
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import os as _os; sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), "..", "src"))
|
|
13
|
+
from melaya import Melaya, MelayaError # noqa: E402
|
|
14
|
+
|
|
15
|
+
KEY = os.environ.get("MK")
|
|
16
|
+
if not KEY:
|
|
17
|
+
print("set MK=mk_..."); sys.exit(2)
|
|
18
|
+
m = Melaya(api_key=KEY)
|
|
19
|
+
# Local dev box does TLS interception; disable cert verification for the test
|
|
20
|
+
# (mirrors NODE_TLS_REJECT_UNAUTHORIZED=0 used by the Node smoke). Not an SDK concern.
|
|
21
|
+
import warnings as _w # noqa: E402
|
|
22
|
+
import httpx as _httpx # noqa: E402
|
|
23
|
+
_w.filterwarnings("ignore")
|
|
24
|
+
m._http = _httpx.Client(base_url="https://api.melaya.org", timeout=30.0,
|
|
25
|
+
headers={"Authorization": f"Bearer {KEY}"}, verify=False)
|
|
26
|
+
import ssl as _ssl # noqa: E402
|
|
27
|
+
_NOVERIFY = _ssl.create_default_context()
|
|
28
|
+
_NOVERIFY.check_hostname = False
|
|
29
|
+
_NOVERIFY.verify_mode = _ssl.CERT_NONE
|
|
30
|
+
SPOT = dict(exchange="binance", symbol="BTC/USDT", market="spot")
|
|
31
|
+
PERP = dict(exchange="binanceusdm", symbol="BTC/USDT:USDT")
|
|
32
|
+
R = []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rec(cat, name, st, d=""):
|
|
36
|
+
R.append((cat, name, st, str(d)[:80]))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def chk(cat, name, fn, validate=None, retry=False):
|
|
40
|
+
for i in range(2 if retry else 1):
|
|
41
|
+
try:
|
|
42
|
+
r = fn()
|
|
43
|
+
if validate is None or validate(r):
|
|
44
|
+
rec(cat, name, "PASS", _short(r)); return r
|
|
45
|
+
if i == (1 if retry else 0):
|
|
46
|
+
rec(cat, name, "FAIL", "invalid shape: " + _short(r)); return r
|
|
47
|
+
except MelayaError as e:
|
|
48
|
+
if i == (1 if retry else 0):
|
|
49
|
+
rec(cat, name, "FAIL", f"{e.status or ''} {e.code or ''} {str(e)[:70]}"); return None
|
|
50
|
+
except Exception as e: # noqa: BLE001
|
|
51
|
+
if i == (1 if retry else 0):
|
|
52
|
+
rec(cat, name, "FAIL", str(e)[:80]); return None
|
|
53
|
+
time.sleep(1.6)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _short(r):
|
|
57
|
+
import json
|
|
58
|
+
try:
|
|
59
|
+
return json.dumps(r)[:80]
|
|
60
|
+
except Exception: # noqa: BLE001
|
|
61
|
+
return str(r)[:80]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_list(n=0):
|
|
65
|
+
return lambda r: isinstance(r, list) and len(r) >= n
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_obj(r):
|
|
69
|
+
return isinstance(r, dict) and len(r) > 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ════ MARKET (22) ════
|
|
73
|
+
chk("market", "list_exchanges", lambda: m.market.list_exchanges(), is_list(60))
|
|
74
|
+
chk("market", "ticker", lambda: m.market.ticker(**SPOT), lambda r: is_obj(r) and (r.get("last") is not None or r.get("bid") is not None), retry=True)
|
|
75
|
+
chk("market", "orderbook", lambda: m.market.orderbook(**SPOT, limit=5), lambda r: r.get("bids"), retry=True)
|
|
76
|
+
chk("market", "ohlcv", lambda: m.market.ohlcv(**SPOT, timeframe="1h", limit=10), is_list(1), retry=True)
|
|
77
|
+
chk("market", "trades", lambda: m.market.trades(**SPOT), is_list(1), retry=True)
|
|
78
|
+
chk("market", "markets", lambda: m.market.markets(exchange="binance"), is_list(1))
|
|
79
|
+
chk("market", "currencies", lambda: m.market.currencies(exchange="kraken"), is_list(1), retry=True)
|
|
80
|
+
chk("market", "status", lambda: m.market.status(exchange="binance"), is_obj)
|
|
81
|
+
chk("market", "time", lambda: m.market.time(exchange="binance"), lambda r: r is not None)
|
|
82
|
+
chk("market", "tickers", lambda: m.market.tickers(exchange="binance", symbols=["BTC/USDT", "ETH/USDT"]), is_obj, retry=True)
|
|
83
|
+
chk("market", "funding_rates", lambda: m.market.funding_rates(exchange="binanceusdm", symbols=[PERP["symbol"]]), is_obj, retry=True)
|
|
84
|
+
chk("market", "funding_rate_history", lambda: m.market.funding_rate_history(exchange="binanceusdm", symbol=PERP["symbol"], hours=24), is_list(1), retry=True)
|
|
85
|
+
chk("market", "open_interest", lambda: m.market.open_interest(exchange="binanceusdm", symbols=[PERP["symbol"]]), is_obj, retry=True)
|
|
86
|
+
chk("market", "open_interest_history", lambda: m.market.open_interest_history(exchange="binanceusdm", symbol=PERP["symbol"], hours=24), is_list(1), retry=True)
|
|
87
|
+
chk("market", "instruments", lambda: m.market.instruments(exchange="binanceusdm"), is_obj)
|
|
88
|
+
chk("market", "liquidation_events", lambda: m.market.liquidation_events(exchange="binanceusdm", limit=10), lambda r: isinstance(r, list))
|
|
89
|
+
chk("market", "ohlcv_multi", lambda: m.market.ohlcv_multi(exchange="binance", symbols=["BTC/USDT", "ETH/USDT"], timeframe="1h", limit=5, market="spot"), is_obj, retry=True)
|
|
90
|
+
chk("market", "market_constraints", lambda: m.market.market_constraints(exchange="binanceusdm", symbol=PERP["symbol"]), lambda r: r is not None)
|
|
91
|
+
chk("market", "funding_rate_history_multi", lambda: m.market.funding_rate_history_multi(exchanges=["binanceusdm", "bybitlinear"], symbol=PERP["symbol"], hours=24), is_obj, retry=True)
|
|
92
|
+
chk("market", "open_interest_history_multi", lambda: m.market.open_interest_history_multi(exchanges=["binanceusdm", "bybitlinear"], symbol=PERP["symbol"], hours=24), is_obj, retry=True)
|
|
93
|
+
chk("market", "prediction_markets", lambda: m.market.prediction_markets(venue="polymarket"), is_list(1), retry=True)
|
|
94
|
+
chk("market", "catalog_counts", lambda: m.market.catalog_counts(), lambda r: r.get("tools", 0) > 0)
|
|
95
|
+
|
|
96
|
+
# ════ ACCOUNT (3) ════
|
|
97
|
+
chk("account", "keys", lambda: m.account.keys(), lambda r: isinstance(r, list))
|
|
98
|
+
chk("account", "usage", lambda: m.account.usage(), lambda r: r.get("tier") is not None)
|
|
99
|
+
chk("account", "api_key_status", lambda: m.account.api_key_status(), is_obj)
|
|
100
|
+
|
|
101
|
+
# ════ STRATEGIES — reads on an existing one; lifecycle on a fresh custom paper one ════
|
|
102
|
+
lst = chk("strategies", "list", lambda: m.strategies.list(), is_list(1))
|
|
103
|
+
read_sid = lst[0]["strategyId"] if lst else None
|
|
104
|
+
if read_sid:
|
|
105
|
+
chk("strategies", "get", lambda: m.strategies.get(read_sid), lambda r: r.get("strategyId") == read_sid)
|
|
106
|
+
chk("strategies", "status", lambda: m.strategies.status(read_sid), is_obj)
|
|
107
|
+
chk("strategies", "executions", lambda: m.strategies.executions(read_sid), lambda r: isinstance(r, list))
|
|
108
|
+
chk("strategies", "trades", lambda: m.strategies.trades(read_sid), lambda r: isinstance(r, list))
|
|
109
|
+
chk("strategies", "performance", lambda: m.strategies.performance(read_sid), lambda r: isinstance(r, list))
|
|
110
|
+
chk("strategies", "logs", lambda: m.strategies.logs(read_sid), lambda r: isinstance(r, list))
|
|
111
|
+
chk("strategies", "ai_opt_status", lambda: m.strategies.ai_opt_status(read_sid), is_obj)
|
|
112
|
+
chk("strategies", "ai_opt_runs", lambda: m.strategies.ai_opt_runs(read_sid), lambda r: r is not None)
|
|
113
|
+
|
|
114
|
+
RHAI = 'fn evaluate() {\n let qty = param("qty");\n if qty == () { qty = 0.001; }\n emit_long(qty);\n}'
|
|
115
|
+
created = chk("strategies", "create(custom,paper)", lambda: m.strategies.create(
|
|
116
|
+
name="SDK full-smoke py (custom)", strategy_type="custom",
|
|
117
|
+
exchange="binanceusdm", symbol="BTC/USDT:USDT", market="FUTURES", dry_run=True,
|
|
118
|
+
params={"language": "rhai", "definition": RHAI, "qty": 0.001},
|
|
119
|
+
), lambda r: r.get("ok") and r.get("strategyId"))
|
|
120
|
+
paper_sid = created.get("strategyId") if created else None
|
|
121
|
+
|
|
122
|
+
if paper_sid:
|
|
123
|
+
chk("strategies", "pause", lambda: m.strategies.pause(paper_sid), lambda r: r.get("ok"))
|
|
124
|
+
chk("strategies", "resume", lambda: m.strategies.resume(paper_sid), lambda r: r.get("ok"))
|
|
125
|
+
chk("strategies", "update_params", lambda: m.strategies.update_params(paper_sid, {"qty": 0.002}), lambda r: r.get("ok"))
|
|
126
|
+
chk("strategies", "ai_opt_stop", lambda: m.strategies.ai_opt_stop(paper_sid), lambda r: r.get("ok"))
|
|
127
|
+
|
|
128
|
+
# ════ SIM (7) ════
|
|
129
|
+
chk("sim", "balance", lambda: m.sim.balance(strategy_id=paper_sid), lambda r: r.get("total") is not None)
|
|
130
|
+
chk("sim", "positions", lambda: m.sim.positions(strategy_id=paper_sid), lambda r: isinstance(r, list))
|
|
131
|
+
chk("sim", "list_accounts", lambda: m.sim.list_accounts(), lambda r: isinstance(r, list))
|
|
132
|
+
chk("sim", "my_trades", lambda: m.sim.my_trades(strategy_id=paper_sid), lambda r: isinstance(r, list))
|
|
133
|
+
px = 60000
|
|
134
|
+
try:
|
|
135
|
+
t = m.market.ticker(**PERP); px = float(t.get("last") or t.get("bid") or 60000)
|
|
136
|
+
except Exception: # noqa: BLE001
|
|
137
|
+
pass
|
|
138
|
+
ordr = chk("sim", "create_order(limit,resting)", lambda: m.sim.create_order(
|
|
139
|
+
strategy_id=paper_sid, exchange="binanceusdm", symbol="BTC/USDT:USDT",
|
|
140
|
+
side="buy", type="limit", price=round(px * 0.5), amount=0.001, market="FUTURES",
|
|
141
|
+
), lambda r: r.get("order_id"))
|
|
142
|
+
chk("sim", "open_orders", lambda: m.sim.open_orders(strategy_id=paper_sid), lambda r: isinstance(r, list))
|
|
143
|
+
if ordr and ordr.get("order_id"):
|
|
144
|
+
oid = ordr["order_id"]
|
|
145
|
+
chk("sim", "cancel_order", lambda: m.sim.cancel_order(strategy_id=paper_sid, order_id=oid, symbol="BTC/USDT:USDT", exchange="binanceusdm"), is_obj)
|
|
146
|
+
else:
|
|
147
|
+
rec("sim", "cancel_order", "SKIP", "no resting order id")
|
|
148
|
+
else:
|
|
149
|
+
for n in ["pause", "resume", "update_params", "ai_opt_stop"]:
|
|
150
|
+
rec("strategies", n, "SKIP", "create failed")
|
|
151
|
+
for n in ["balance", "positions", "list_accounts", "my_trades", "create_order(limit,resting)", "open_orders", "cancel_order"]:
|
|
152
|
+
rec("sim", n, "SKIP", "no paper sid")
|
|
153
|
+
|
|
154
|
+
rec("strategies", "ai_opt_start", "WIRED", "not invoked (billed optimization)")
|
|
155
|
+
rec("strategies", "ai_opt_approve", "WIRED", "not invoked (applies optimizer output)")
|
|
156
|
+
|
|
157
|
+
# ════ BACKTEST (custom strategy, end-to-end; deleteAll skipped) ════
|
|
158
|
+
now = int(time.time() * 1000)
|
|
159
|
+
bt = chk("backtest", "start(custom)", lambda: m.backtest.start({
|
|
160
|
+
"strategyType": "custom", "exchange": "binance", "symbol": "BTC/USDT", "timeframe": "1h",
|
|
161
|
+
"since_ms": now - 60 * 86400000, "until_ms": now, "initial_equity": 10000,
|
|
162
|
+
"language": "rhai", "definition": RHAI, "custom_code": RHAI, "params": {"qty": 0.001},
|
|
163
|
+
}), lambda r: r.get("job_id"))
|
|
164
|
+
job_id = bt.get("job_id") if bt else None
|
|
165
|
+
if job_id:
|
|
166
|
+
status = "pending"
|
|
167
|
+
for _ in range(20):
|
|
168
|
+
if status in ("done", "error", "halted", "cancelled"):
|
|
169
|
+
break
|
|
170
|
+
time.sleep(2)
|
|
171
|
+
try:
|
|
172
|
+
status = str(m.backtest.job(job_id).get("status", "")).lower()
|
|
173
|
+
except Exception: # noqa: BLE001
|
|
174
|
+
pass
|
|
175
|
+
chk("backtest", "job(poll)", lambda: m.backtest.job(job_id), lambda r: r.get("job_id") == job_id)
|
|
176
|
+
if status == "done":
|
|
177
|
+
chk("backtest", "results", lambda: m.backtest.results(job_id), is_obj)
|
|
178
|
+
chk("backtest", "trades", lambda: m.backtest.trades(job_id, limit=10), lambda r: isinstance(r, list))
|
|
179
|
+
else:
|
|
180
|
+
rec("backtest", "results", "SKIP", f"job {status}")
|
|
181
|
+
rec("backtest", "trades", "SKIP", f"job {status}")
|
|
182
|
+
chk("backtest", "list", lambda: m.backtest.list(limit=5), lambda r: isinstance(r, list))
|
|
183
|
+
chk("backtest", "favorites", lambda: m.backtest.favorites(limit=5), lambda r: isinstance(r, list))
|
|
184
|
+
chk("backtest", "funding_range", lambda: m.backtest.funding_range(exchange="binanceusdm", symbol=PERP["symbol"]), lambda r: r is None or isinstance(r, (int, float)))
|
|
185
|
+
sweep = chk("backtest", "start(grid_sweep)", lambda: m.backtest.start({
|
|
186
|
+
"mode": "grid_sweep", "strategyType": "custom", "exchange": "binance", "symbol": "BTC/USDT", "timeframe": "1h",
|
|
187
|
+
"since_ms": now - 30 * 86400000, "until_ms": now, "language": "rhai", "definition": RHAI, "custom_code": RHAI,
|
|
188
|
+
"paramRanges": {"qty": [0.001, 0.002]},
|
|
189
|
+
}), lambda r: r.get("job_id"))
|
|
190
|
+
if sweep and sweep.get("job_id"):
|
|
191
|
+
chk("backtest", "sweep", lambda: m.backtest.sweep(sweep["job_id"], limit=10), is_obj)
|
|
192
|
+
else:
|
|
193
|
+
rec("backtest", "sweep", "SKIP", "no sweep parent")
|
|
194
|
+
cj = chk("backtest", "start(for-cancel)", lambda: m.backtest.start({
|
|
195
|
+
"strategyType": "custom", "exchange": "binance", "symbol": "ETH/USDT", "timeframe": "1h",
|
|
196
|
+
"since_ms": now - 365 * 86400000, "until_ms": now, "language": "rhai", "definition": RHAI, "custom_code": RHAI, "params": {"qty": 0.001},
|
|
197
|
+
}), lambda r: r.get("job_id"))
|
|
198
|
+
if cj and cj.get("job_id"):
|
|
199
|
+
chk("backtest", "cancel", lambda: m.backtest.cancel(cj["job_id"]), is_obj)
|
|
200
|
+
chk("backtest", "delete", lambda: m.backtest.delete(cj["job_id"]), lambda r: r.get("ok"))
|
|
201
|
+
else:
|
|
202
|
+
rec("backtest", "cancel", "SKIP", "no job"); rec("backtest", "delete", "SKIP", "no job")
|
|
203
|
+
rec("backtest", "delete_all", "WIRED", "not invoked (soft-deletes ALL non-favorited jobs)")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ════ STREAMS — public (5) + private (2) ════
|
|
207
|
+
async def stream_chk(cat, name, factory):
|
|
208
|
+
opened = False
|
|
209
|
+
try:
|
|
210
|
+
agen = factory().__aiter__()
|
|
211
|
+
frame = await asyncio.wait_for(agen.__anext__(), timeout=10)
|
|
212
|
+
rec(cat, name, "PASS", "frame " + _short(frame)[:45])
|
|
213
|
+
except asyncio.TimeoutError:
|
|
214
|
+
rec(cat, name, "PASS" if opened else "FAIL", "no frame in 10s")
|
|
215
|
+
except Exception as e: # noqa: BLE001
|
|
216
|
+
rec(cat, name, "FAIL", str(e)[:60])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def run_streams():
|
|
220
|
+
await stream_chk("stream", "ticker", lambda: m.stream.ticker(**SPOT))
|
|
221
|
+
await stream_chk("stream", "orderbook", lambda: m.stream.orderbook(**SPOT, limit=10))
|
|
222
|
+
await stream_chk("stream", "ohlcv", lambda: m.stream.ohlcv(**SPOT, timeframe="1m"))
|
|
223
|
+
await stream_chk("stream", "trades", lambda: m.stream.trades(**SPOT))
|
|
224
|
+
await stream_chk("stream", "liquidations", lambda: m.stream.liquidations(exchange="binanceusdm"))
|
|
225
|
+
await stream_chk("stream", "strategies(private)", lambda: m.stream.strategies())
|
|
226
|
+
try:
|
|
227
|
+
keys = m.account.keys()
|
|
228
|
+
except Exception: # noqa: BLE001
|
|
229
|
+
keys = []
|
|
230
|
+
if keys:
|
|
231
|
+
k = keys[0]
|
|
232
|
+
await stream_chk("stream", "private(account)", lambda: m.stream.private(exchange=k.get("exchange"), market=k.get("market"), api_key_id=k.get("apiKeyId")))
|
|
233
|
+
else:
|
|
234
|
+
rec("stream", "private(account)", "SKIP", "no connected key")
|
|
235
|
+
|
|
236
|
+
asyncio.run(run_streams())
|
|
237
|
+
|
|
238
|
+
# ════ TRADE — live credentialed reads (5); write ops WIRED — real funds ════
|
|
239
|
+
_VEN = dict(exchange="bitgetfutures", api_key_id="BITGETFUTURES_0", market_type="futures")
|
|
240
|
+
chk("trade", "balance", lambda: m.trade.balance(**_VEN), lambda r: r.get("ok") is True)
|
|
241
|
+
chk("trade", "positions", lambda: m.trade.positions(**_VEN), lambda r: r.get("ok") is True)
|
|
242
|
+
chk("trade", "open_orders", lambda: m.trade.open_orders(**_VEN), lambda r: r.get("ok") is True)
|
|
243
|
+
chk("trade", "orders", lambda: m.trade.orders(**_VEN), lambda r: r.get("ok") is True)
|
|
244
|
+
chk("trade", "my_trades", lambda: m.trade.my_trades(**_VEN, symbol="BTC/USDT:USDT"), lambda r: r.get("ok") is True)
|
|
245
|
+
for _w in ["create_order","cancel_order","amend_order","cancel_all_orders","cancel_plan_orders","close_position","set_leverage","set_margin_mode","set_position_mode"]:
|
|
246
|
+
rec("trade", _w, "WIRED", "not invoked — LIVE write, real funds")
|
|
247
|
+
|
|
248
|
+
# ════ TEARDOWN ════
|
|
249
|
+
if paper_sid:
|
|
250
|
+
chk("teardown", "strategies.stop", lambda: m.strategies.stop(paper_sid), lambda r: r.get("ok"))
|
|
251
|
+
chk("teardown", "strategies.delete", lambda: m.strategies.delete(paper_sid), lambda r: r.get("ok"))
|
|
252
|
+
|
|
253
|
+
m.close()
|
|
254
|
+
|
|
255
|
+
# ════ REPORT ════
|
|
256
|
+
print("\n============== MELAYA SDK — FULL ENDPOINT VALIDATION (Python) ==============")
|
|
257
|
+
cats = []
|
|
258
|
+
for c, *_ in R:
|
|
259
|
+
if c not in cats:
|
|
260
|
+
cats.append(c)
|
|
261
|
+
nP = nF = nW = nS = 0
|
|
262
|
+
for cat in cats:
|
|
263
|
+
print(f"\n-- {cat} --")
|
|
264
|
+
for c, name, st, d in [x for x in R if x[0] == cat]:
|
|
265
|
+
print(f" {st:<5} {name:<28} {d}")
|
|
266
|
+
nP += st == "PASS"; nF += st == "FAIL"; nW += st == "WIRED"; nS += st == "SKIP"
|
|
267
|
+
print("\n===========================================================================")
|
|
268
|
+
print(f"PASS {nP} FAIL {nF} WIRED(not-invoked) {nW} SKIP {nS} | total {nP + nF + nW + nS}")
|
|
269
|
+
print("RESULT: GO — every invoked endpoint validated." if nF == 0 else f"RESULT: NO-GO — {nF} failing.")
|
|
270
|
+
sys.exit(0 if nF == 0 else 1)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "melaya"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the Melaya unified market-data & streaming API across 70+ venues."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "Melaya" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"melaya",
|
|
15
|
+
"trading",
|
|
16
|
+
"crypto",
|
|
17
|
+
"market-data",
|
|
18
|
+
"exchange",
|
|
19
|
+
"websocket",
|
|
20
|
+
"orderbook",
|
|
21
|
+
"ohlcv",
|
|
22
|
+
"ticker",
|
|
23
|
+
"agentic-ai",
|
|
24
|
+
"sdk",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
30
|
+
"License :: OSI Approved :: Apache Software License",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
33
|
+
]
|
|
34
|
+
dependencies = ["httpx>=0.27"]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
stream = ["websockets>=12"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://melaya.org"
|
|
41
|
+
Documentation = "https://melaya.org/docs"
|
|
42
|
+
Repository = "https://github.com/melaya-labs/melaya"
|
|
43
|
+
Issues = "https://github.com/melaya-labs/melaya/issues"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/melaya"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Official Python SDK for the Melaya unified market-data & streaming API.
|
|
2
|
+
|
|
3
|
+
>>> from melaya import Melaya
|
|
4
|
+
>>> m = Melaya(api_key="mk_...")
|
|
5
|
+
>>> m.market.ticker(exchange="binance", symbol="BTC/USDT", market="spot")
|
|
6
|
+
|
|
7
|
+
See https://melaya.org/docs
|
|
8
|
+
"""
|
|
9
|
+
from .client import Melaya, DEFAULT_BASE_URL, DEFAULT_WS_URL
|
|
10
|
+
from .errors import MelayaError
|
|
11
|
+
|
|
12
|
+
__all__ = ["Melaya", "MelayaError", "DEFAULT_BASE_URL", "DEFAULT_WS_URL"]
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Account API — authenticated reads about your Melaya account.
|
|
2
|
+
|
|
3
|
+
Connected-exchange key references (masked), tier limits, and live usage
|
|
4
|
+
counters. Requires an ``mk_`` key on the private plane.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Callable, Dict, List
|
|
9
|
+
|
|
10
|
+
_Request = Callable[..., Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccountAPI:
|
|
14
|
+
def __init__(self, request: _Request) -> None:
|
|
15
|
+
self._request = request
|
|
16
|
+
|
|
17
|
+
def keys(self) -> List[Dict[str, Any]]:
|
|
18
|
+
"""Connected exchange keys. ``apiKey`` is masked; use ``apiKeyId`` as the reference."""
|
|
19
|
+
return self._request("GET", "/api/v1/private/keys")["keys"]
|
|
20
|
+
|
|
21
|
+
def usage(self) -> Dict[str, Any]:
|
|
22
|
+
"""Tier, plan limits, and live usage counters."""
|
|
23
|
+
return self._request("GET", "/api/v1/private/usage")
|
|
24
|
+
|
|
25
|
+
def api_key_status(self) -> Dict[str, Any]:
|
|
26
|
+
"""Status of your platform API key (tier, max concurrent connections)."""
|
|
27
|
+
return self._request("GET", "/api/v1/private/api-key")
|