marginpad 2.9.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.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: marginpad
3
+ Version: 2.9.0
4
+ Summary: MarginPad Bot API client: free crypto-futures paper trading for bots (REST + WebSocket + webhooks) and free market data. No dependencies.
5
+ Author-email: MarginPad <support@marginpad.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://marginpad.io/trading-api/
8
+ Project-URL: Documentation, https://marginpad.io/api-docs/
9
+ Project-URL: Repository, https://github.com/marginpad/sdk
10
+ Project-URL: Changelog, https://marginpad.io/api/changelog.xml
11
+ Keywords: crypto,futures,paper-trading,trading-bot,backtest,liquidations,api,mcp,ai-agent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Office/Business :: Financial :: Investment
15
+ Classifier: Intended Audience :: Developers
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Provides-Extra: stream
19
+ Requires-Dist: websockets>=11; extra == "stream"
20
+
21
+ # MarginPad SDK
22
+
23
+ Free crypto-futures **paper trading for bots** and free market data. One key, no KYC, no deposit.
24
+ Test a strategy or an AI agent against live prices with real fee schedules, funding, liquidations, limit and stop orders, trailing stops, webhooks and a WebSocket stream, then take the same code to a real exchange.
25
+
26
+ - Guide: https://marginpad.io/trading-api/
27
+ - Reference (OpenAPI, try it live): https://marginpad.io/api-docs/
28
+ - Status: https://marginpad.io/status/ - Changelog: https://marginpad.io/api/changelog.xml
29
+ - MCP server for Claude, ChatGPT and Cursor: https://marginpad.io/trading-api/#mcp
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install marginpad # Python 3.8+, no dependencies (pip install "marginpad[stream]" for the WebSocket)
35
+ npm install marginpad # Node 18+, zero dependencies
36
+ ```
37
+
38
+ ## Python
39
+
40
+ ```python
41
+ from marginpad import MarginPad
42
+
43
+ mp = MarginPad("mpb_...") # key from https://marginpad.io/trading-api/
44
+ print(mp.price("BTC")) # keyless market data
45
+ pos = mp.open("BTC", "long", margin_usd=100, leverage=10, sl=58000, tp=66000,
46
+ client_order_id="sig-2026-09-12-1403") # idempotent: a retry returns the same position
47
+ mp.sltp(pos["id"], trail_pct=1.5) # trailing stop, ratcheted server-side
48
+ print(mp.account())
49
+ ```
50
+
51
+ ## JavaScript
52
+
53
+ ```js
54
+ const { MarginPad } = require('marginpad');
55
+ const mp = new MarginPad('mpb_...');
56
+ const { position } = await mp.open({ symbol: 'BTC', side: 'long', margin_usd: 100, leverage: 10, client_order_id: 'sig-1' });
57
+ mp.stream(ev => console.log(ev.type, ev.data)); // positions pushed on change, prices each tick
58
+ ```
59
+
60
+ ## Examples
61
+
62
+ - `examples/agent_loop.py`: a signal loop that opens, manages and closes positions once a minute without polling storms.
63
+ - `examples/webhook_server.js`: receive `position.closed` / `order.filled` events with signature verification.
64
+ - `examples/mcp.md`: let Claude Desktop or Cursor trade on your paper account through the MCP server.
65
+
66
+ ## Limits
67
+
68
+ Free: 120 requests/minute per key, 3 keys, 50 open positions. Premium: 600/minute, 10 keys, 200 open positions.
69
+ Every response carries `X-RateLimit-Limit / Remaining / Reset`; on 429 both clients wait `Retry-After` and retry once.
70
+ Out-of-range input is refused with a named error (`leverage_max` carries the market's cap), never silently clamped.
71
+
72
+ ## Honesty note
73
+
74
+ This is a simulator. Fees, funding and liquidation math mirror real exchanges; there is no order book, so slippage is modelled, not measured on depth. A bot that passes here still needs a small live test. Educational use, not financial advice.
75
+
76
+ MIT licensed.
@@ -0,0 +1,56 @@
1
+ # MarginPad SDK
2
+
3
+ Free crypto-futures **paper trading for bots** and free market data. One key, no KYC, no deposit.
4
+ Test a strategy or an AI agent against live prices with real fee schedules, funding, liquidations, limit and stop orders, trailing stops, webhooks and a WebSocket stream, then take the same code to a real exchange.
5
+
6
+ - Guide: https://marginpad.io/trading-api/
7
+ - Reference (OpenAPI, try it live): https://marginpad.io/api-docs/
8
+ - Status: https://marginpad.io/status/ - Changelog: https://marginpad.io/api/changelog.xml
9
+ - MCP server for Claude, ChatGPT and Cursor: https://marginpad.io/trading-api/#mcp
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install marginpad # Python 3.8+, no dependencies (pip install "marginpad[stream]" for the WebSocket)
15
+ npm install marginpad # Node 18+, zero dependencies
16
+ ```
17
+
18
+ ## Python
19
+
20
+ ```python
21
+ from marginpad import MarginPad
22
+
23
+ mp = MarginPad("mpb_...") # key from https://marginpad.io/trading-api/
24
+ print(mp.price("BTC")) # keyless market data
25
+ pos = mp.open("BTC", "long", margin_usd=100, leverage=10, sl=58000, tp=66000,
26
+ client_order_id="sig-2026-09-12-1403") # idempotent: a retry returns the same position
27
+ mp.sltp(pos["id"], trail_pct=1.5) # trailing stop, ratcheted server-side
28
+ print(mp.account())
29
+ ```
30
+
31
+ ## JavaScript
32
+
33
+ ```js
34
+ const { MarginPad } = require('marginpad');
35
+ const mp = new MarginPad('mpb_...');
36
+ const { position } = await mp.open({ symbol: 'BTC', side: 'long', margin_usd: 100, leverage: 10, client_order_id: 'sig-1' });
37
+ mp.stream(ev => console.log(ev.type, ev.data)); // positions pushed on change, prices each tick
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ - `examples/agent_loop.py`: a signal loop that opens, manages and closes positions once a minute without polling storms.
43
+ - `examples/webhook_server.js`: receive `position.closed` / `order.filled` events with signature verification.
44
+ - `examples/mcp.md`: let Claude Desktop or Cursor trade on your paper account through the MCP server.
45
+
46
+ ## Limits
47
+
48
+ Free: 120 requests/minute per key, 3 keys, 50 open positions. Premium: 600/minute, 10 keys, 200 open positions.
49
+ Every response carries `X-RateLimit-Limit / Remaining / Reset`; on 429 both clients wait `Retry-After` and retry once.
50
+ Out-of-range input is refused with a named error (`leverage_max` carries the market's cap), never silently clamped.
51
+
52
+ ## Honesty note
53
+
54
+ This is a simulator. Fees, funding and liquidation math mirror real exchanges; there is no order book, so slippage is modelled, not measured on depth. A bot that passes here still needs a small live test. Educational use, not financial advice.
55
+
56
+ MIT licensed.
@@ -0,0 +1,307 @@
1
+ """MarginPad Bot API client - one file, no dependencies (Python 3.8+).
2
+
3
+ from marginpad import MarginPad
4
+ mp = MarginPad("mpb_...") # key from https://marginpad.io/trading-api/
5
+ print(mp.price("BTC")) # keyless market data
6
+ pos = mp.open("BTC", "long", margin_usd=100, leverage=10, sl=58000, tp=66000,
7
+ client_order_id="sig-2026-09-11-1403") # idempotent: a retry returns the same position
8
+ mp.sltp(pos["id"], trail_pct=1.5) # trailing stop, ratcheted server-side
9
+ print(mp.account())
10
+
11
+ Every method returns the `data` part of the v2 envelope as a dict/list and raises MarginPadError on
12
+ {"ok": false}. Rate limits: the client sleeps and retries ONCE on 429 when auto_retry=True (default).
13
+ WebSocket stream (`stream()`) needs the optional `websockets` package: pip install websockets.
14
+ Docs: https://marginpad.io/trading-api/ · Spec: https://marginpad.io/api/openapi.json
15
+ """
16
+ import hashlib
17
+ import hmac
18
+ import json
19
+ import time
20
+ import urllib.error
21
+ import urllib.parse
22
+ import urllib.request
23
+
24
+ __version__ = "2.6.0"
25
+ __all__ = ["MarginPad", "MarginPadError", "verify_webhook"]
26
+
27
+
28
+ class MarginPadError(Exception):
29
+ """Raised for every {"ok": false} answer. .code is stable, .message is prose, .status is the HTTP status."""
30
+
31
+ def __init__(self, code, message="", status=0, extra=None):
32
+ super().__init__("%s: %s" % (code, message or code))
33
+ self.code = code
34
+ self.message = message
35
+ self.status = status
36
+ self.extra = extra or {}
37
+
38
+
39
+ def verify_webhook(secret, raw_body, signature_header):
40
+ """True when X-MP-Signature matches HMAC-SHA256(secret, raw body). Compare BEFORE parsing the body."""
41
+ if isinstance(raw_body, str):
42
+ raw_body = raw_body.encode("utf-8")
43
+ want = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
44
+ return hmac.compare_digest(want, str(signature_header or ""))
45
+
46
+
47
+ class MarginPad(object):
48
+ def __init__(self, api_key=None, base="https://marginpad.io", timeout=15, auto_retry=True, user_agent=None):
49
+ self.api_key = api_key
50
+ self.base = base.rstrip("/")
51
+ self.timeout = timeout
52
+ self.auto_retry = auto_retry
53
+ self.user_agent = user_agent or ("marginpad-python/" + __version__)
54
+ self.last_headers = {}
55
+ self._etag = None
56
+
57
+ # ── transport ────────────────────────────────────────────────────────────────────────────────────────
58
+ def _call(self, method, path, params=None, body=None, keyed=True, headers=None, _retried=False):
59
+ url = self.base + path
60
+ if params:
61
+ q = {k: v for k, v in params.items() if v is not None}
62
+ if q:
63
+ url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
64
+ h = {"accept": "application/json", "user-agent": self.user_agent}
65
+ if keyed and self.api_key:
66
+ h["x-api-key"] = self.api_key
67
+ if headers:
68
+ h.update(headers)
69
+ data = None
70
+ if body is not None:
71
+ data = json.dumps(body).encode("utf-8")
72
+ h["content-type"] = "application/json"
73
+ req = urllib.request.Request(url, data=data, method=method, headers=h)
74
+ try:
75
+ with urllib.request.urlopen(req, timeout=self.timeout) as r:
76
+ self.last_headers = dict(r.headers)
77
+ if r.status == 304:
78
+ return None
79
+ raw = r.read().decode("utf-8")
80
+ except urllib.error.HTTPError as e:
81
+ self.last_headers = dict(e.headers)
82
+ if e.code == 304:
83
+ return None
84
+ if e.code == 429 and self.auto_retry and not _retried:
85
+ wait = e.headers.get("retry-after")
86
+ time.sleep(min(60, max(1, int(wait) if wait and wait.isdigit() else 5)))
87
+ return self._call(method, path, params, body, keyed, headers, _retried=True)
88
+ raw = e.read().decode("utf-8", "replace")
89
+ try:
90
+ j = json.loads(raw)
91
+ except ValueError:
92
+ raise MarginPadError("http_%d" % e.code, raw[:200], e.code)
93
+ err = j.get("error") if isinstance(j, dict) else None
94
+ if isinstance(err, dict):
95
+ raise MarginPadError(err.get("code", "error"), err.get("message", ""), e.code, err)
96
+ raise MarginPadError(str(err or "http_%d" % e.code), j.get("hint", "") if isinstance(j, dict) else "", e.code, j)
97
+ j = json.loads(raw)
98
+ if isinstance(j, dict) and j.get("ok") is False:
99
+ err = j.get("error") or {}
100
+ raise MarginPadError(err.get("code", "error"), err.get("message", ""), 200, err)
101
+ return j.get("data", j) if isinstance(j, dict) else j
102
+
103
+ def _get(self, path, keyed=True, **params):
104
+ return self._call("GET", path, params=params, keyed=keyed)
105
+
106
+ def _post(self, path, **body):
107
+ return self._call("POST", path, body={k: v for k, v in body.items() if v is not None})
108
+
109
+ # ── market data (keyless; send the key anyway and it counts against your per-key budget) ────────────
110
+ def price(self, symbol):
111
+ return self._get("/api/v1/price", symbol=symbol)
112
+
113
+ def prices(self):
114
+ return self._get("/api/v1/prices")
115
+
116
+ def klines(self, symbol, interval=60, end_ms=None):
117
+ return self._get("/api/v1/klines", symbol=symbol, interval=interval, end=end_ms)
118
+
119
+ def markets(self, asset_class=None):
120
+ return self._get("/api/bot/v2/markets", **{"class": asset_class})
121
+
122
+ def screener(self):
123
+ return self._get("/api/v1/screener")
124
+
125
+ def funding(self):
126
+ return self._get("/api/v1/funding")
127
+
128
+ def open_interest(self):
129
+ return self._get("/api/v1/open-interest")
130
+
131
+ def liquidations(self):
132
+ return self._get("/api/v1/liquidations")
133
+
134
+ def fear_greed(self):
135
+ return self._get("/api/v1/fear-greed")
136
+
137
+ def calendar(self, year=None):
138
+ return self._get("/api/v1/calendar", year=year)
139
+
140
+ def server_time(self):
141
+ return self._get("/api/bot/v2/time", keyed=False, client_ts=int(time.time() * 1000))
142
+
143
+ # ── paper trading ───────────────────────────────────────────────────────────────────────────────────
144
+ def open(self, symbol, side, margin_usd, leverage, sl=None, tp=None, trail_pct=None, client_order_id=None, dry_run=False,
145
+ slippage=None, margin_tiers=None, mmr_pct=None, fee_venue=None):
146
+ """Market open at the live price. dry_run=True prices the trade and writes nothing.
147
+
148
+ slippage / margin_tiers / mmr_pct override this account's realism setting for THIS fill only (see realism()).
149
+ fee_venue charges this one position at a named venue's taker schedule (see fees())."""
150
+ return self._post("/api/bot/v2/open", symbol=symbol, side=side, margin_usd=margin_usd, leverage=leverage, sl=sl, tp=tp, trail_pct=trail_pct, client_order_id=client_order_id, dry_run=True if dry_run else None,
151
+ slippage=slippage, margin_tiers=margin_tiers, mmr_pct=mmr_pct, fee_venue=fee_venue)
152
+
153
+ def limit_order(self, symbol, side, limit_price, margin_usd, leverage, sl=None, tp=None, trail_pct=None, client_order_id=None, dry_run=False):
154
+ """Pullback entry: a long below the market, a short above it. Fills AT the level from 1m candles."""
155
+ return self._post("/api/bot/v2/open", type="limit", symbol=symbol, side=side, limit_price=limit_price, margin_usd=margin_usd, leverage=leverage, sl=sl, tp=tp, trail_pct=trail_pct, client_order_id=client_order_id, dry_run=True if dry_run else None)
156
+
157
+ def stop_order(self, symbol, side, stop_price, margin_usd, leverage, sl=None, tp=None, trail_pct=None, client_order_id=None, dry_run=False):
158
+ """Breakout entry: a long above the market, a short below it."""
159
+ return self._post("/api/bot/v2/open", type="stop", symbol=symbol, side=side, limit_price=stop_price, margin_usd=margin_usd, leverage=leverage, sl=sl, tp=tp, trail_pct=trail_pct, client_order_id=client_order_id, dry_run=True if dry_run else None)
160
+
161
+ def orders(self):
162
+ return self._get("/api/bot/v2/orders")
163
+
164
+ def modify_order(self, order_id, limit_price=None, sl=None, tp=None, margin_usd=None, leverage=None, trail_pct=None):
165
+ return self._call("POST", "/api/bot/v2/modify_order", body={"order_id": order_id, **{k: v for k, v in dict(limit_price=limit_price, sl=sl, tp=tp, margin_usd=margin_usd, leverage=leverage, trail_pct=trail_pct).items() if v is not None}})
166
+
167
+ def cancel_order(self, order_id):
168
+ return self._post("/api/bot/v2/cancel_order", order_id=order_id)
169
+
170
+ def close(self, position_id, symbol=None, pct=None, client_order_id=None):
171
+ """Pass symbol (every position carries it): one price fetch, one round trip."""
172
+ return self._post("/api/bot/v2/close", id=position_id, symbol=symbol, pct=pct, client_order_id=client_order_id)
173
+
174
+ def close_all(self):
175
+ return self._call("POST", "/api/bot/v2/close_all", body={})
176
+
177
+ def sltp(self, position_id, sl="keep", tp="keep", trail_pct="keep"):
178
+ """Move SL / TP / trailing stop. None clears a level; omit to keep it."""
179
+ body = {"id": position_id}
180
+ if sl != "keep":
181
+ body["sl"] = sl
182
+ if tp != "keep":
183
+ body["tp"] = tp
184
+ if trail_pct != "keep":
185
+ body["trail_pct"] = trail_pct
186
+ return self._call("POST", "/api/bot/v2/sltp", body=body)
187
+
188
+ def positions(self, status=None, since_ms=None, use_etag=True):
189
+ """Returns None when nothing changed since the last call (304) and use_etag is on."""
190
+ h = {"if-none-match": self._etag} if (use_etag and self._etag) else None
191
+ d = self._call("GET", "/api/bot/v2/positions", params={"status": status, "since": since_ms}, headers=h)
192
+ et = self.last_headers.get("etag") or self.last_headers.get("ETag")
193
+ if et:
194
+ self._etag = et
195
+ return d
196
+
197
+ def trades(self, limit=100, before_ms=None):
198
+ return self._get("/api/bot/v2/trades", limit=limit, before=before_ms)
199
+
200
+ def account(self):
201
+ return self._get("/api/bot/v2/account")
202
+
203
+ def balance(self):
204
+ return self._get("/api/bot/v2/balance")
205
+
206
+ def usage(self):
207
+ return self._get("/api/bot/v2/usage")
208
+
209
+ def report(self, days=30):
210
+ return self._get("/api/bot/v2/report", days=days)
211
+
212
+ # 2.5 books, reset, equity · 2.6 replay
213
+ def accounts(self):
214
+ return self._get("/api/bot/v2/accounts")
215
+
216
+ def reset(self):
217
+ """Restart this key's book at $10,000 (closed trades archived, orders cancelled). Refused while positions are open."""
218
+ return self._post("/api/bot/v2/reset", confirm=True)
219
+
220
+ def equity(self, days=30, step_min=None):
221
+ return self._get("/api/bot/v2/equity", days=days, step_min=step_min)
222
+
223
+ def replay_start(self, symbol, day, speed=60):
224
+ """Run this key through one past UTC day (YYYY-MM-DD) on MarginPad's 1-minute candles. speed = market seconds per real second (1-600)."""
225
+ return self._post("/api/bot/v2/replay", symbol=symbol, day=day, speed=speed)
226
+
227
+ def replay(self, interval=None, bars=None):
228
+ """Replay status: cursor, price, progress and the candles up to the cursor."""
229
+ return self._get("/api/bot/v2/replay", interval=interval, bars=bars)
230
+
231
+ def replay_stop(self):
232
+ return self._post("/api/bot/v2/replay", act="stop")
233
+
234
+ def ai(self, symbol, interval=60, question=None, lang=None):
235
+ """Premium: the chart panel's AI read. Educational, not advice."""
236
+ return self._post("/api/bot/v2/ai", symbol=symbol, interval=str(interval), question=question, lang=lang)
237
+
238
+ # ── webhooks (Premium) ──────────────────────────────────────────────────────────────────────────────
239
+ # -- fee schedule and fill realism (2.9) ------------------------------------------------------------
240
+ def fees(self, venue=None):
241
+ """Read the fee schedules, or set which venue's taker fee your fills are charged at.
242
+ fees() -> the table plus your default. fees("binance") -> sets it. fees("") -> back to our rate."""
243
+ if venue is None:
244
+ return self._get("/api/bot/v2/fees")
245
+ return self._post("/api/bot/v2/fees", venue=venue)
246
+
247
+ def realism(self, slippage=None, margin_tiers=None, margin_venue=None):
248
+ """Read or set fill realism. Both switches are OFF by default: a market order fills at the live
249
+ price and maintenance margin is a flat 0.5%, which is easier than a real venue and increasingly so as
250
+ a position grows. Reading also returns the whole size-tier ladder and every venue's margin rate, so
251
+ you never have to work out where a liquidation price came from.
252
+
253
+ c.realism() # what am I on, and what are the tiers
254
+ c.realism(slippage=True, margin_tiers=True) # test against something closer to a real book
255
+ c.realism(margin_venue="binance") # liquidate where Binance would
256
+ """
257
+ if slippage is None and margin_tiers is None and margin_venue is None:
258
+ return self._get("/api/bot/v2/realism")
259
+ body = {}
260
+ if slippage is not None:
261
+ body["slippage"] = bool(slippage)
262
+ if margin_tiers is not None:
263
+ body["margin_tiers"] = bool(margin_tiers)
264
+ if margin_venue is not None:
265
+ body["margin_venue"] = margin_venue
266
+ return self._post("/api/bot/v2/realism", **body)
267
+
268
+ def webhooks(self):
269
+ return self._get("/api/bot/v2/webhooks")
270
+
271
+ def add_webhook(self, url, events=None):
272
+ return self._post("/api/bot/v2/webhooks", act="add", url=url, events=events)
273
+
274
+ def delete_webhook(self, webhook_id):
275
+ return self._post("/api/bot/v2/webhooks", act="delete", id=webhook_id)
276
+
277
+ def test_webhook(self, webhook_id):
278
+ return self._post("/api/bot/v2/webhooks", act="test", id=webhook_id)
279
+
280
+ # ── WebSocket stream (optional dependency: websockets) ──────────────────────────────────────────────
281
+ def stream(self, on_event, channels=("positions", "prices"), reconnect=True):
282
+ """Blocks; calls on_event(dict) for every message. pip install websockets."""
283
+ import asyncio
284
+ try:
285
+ import websockets
286
+ except ImportError:
287
+ raise RuntimeError("pip install websockets (needed only for stream())")
288
+ url = self.base.replace("https://", "wss://").replace("http://", "ws://") + "/api/bot/v2/stream?api_key=" + urllib.parse.quote(self.api_key or "")
289
+
290
+ async def run():
291
+ while True:
292
+ try:
293
+ async with websockets.connect(url, ping_interval=25) as ws:
294
+ await ws.send(json.dumps({"op": "subscribe", "channels": list(channels)}))
295
+ async for raw in ws:
296
+ try:
297
+ on_event(json.loads(raw))
298
+ except Exception:
299
+ pass
300
+ except Exception:
301
+ if not reconnect:
302
+ raise
303
+ await asyncio.sleep(3)
304
+ if not reconnect:
305
+ return
306
+
307
+ asyncio.get_event_loop().run_until_complete(run())
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: marginpad
3
+ Version: 2.9.0
4
+ Summary: MarginPad Bot API client: free crypto-futures paper trading for bots (REST + WebSocket + webhooks) and free market data. No dependencies.
5
+ Author-email: MarginPad <support@marginpad.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://marginpad.io/trading-api/
8
+ Project-URL: Documentation, https://marginpad.io/api-docs/
9
+ Project-URL: Repository, https://github.com/marginpad/sdk
10
+ Project-URL: Changelog, https://marginpad.io/api/changelog.xml
11
+ Keywords: crypto,futures,paper-trading,trading-bot,backtest,liquidations,api,mcp,ai-agent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Office/Business :: Financial :: Investment
15
+ Classifier: Intended Audience :: Developers
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ Provides-Extra: stream
19
+ Requires-Dist: websockets>=11; extra == "stream"
20
+
21
+ # MarginPad SDK
22
+
23
+ Free crypto-futures **paper trading for bots** and free market data. One key, no KYC, no deposit.
24
+ Test a strategy or an AI agent against live prices with real fee schedules, funding, liquidations, limit and stop orders, trailing stops, webhooks and a WebSocket stream, then take the same code to a real exchange.
25
+
26
+ - Guide: https://marginpad.io/trading-api/
27
+ - Reference (OpenAPI, try it live): https://marginpad.io/api-docs/
28
+ - Status: https://marginpad.io/status/ - Changelog: https://marginpad.io/api/changelog.xml
29
+ - MCP server for Claude, ChatGPT and Cursor: https://marginpad.io/trading-api/#mcp
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install marginpad # Python 3.8+, no dependencies (pip install "marginpad[stream]" for the WebSocket)
35
+ npm install marginpad # Node 18+, zero dependencies
36
+ ```
37
+
38
+ ## Python
39
+
40
+ ```python
41
+ from marginpad import MarginPad
42
+
43
+ mp = MarginPad("mpb_...") # key from https://marginpad.io/trading-api/
44
+ print(mp.price("BTC")) # keyless market data
45
+ pos = mp.open("BTC", "long", margin_usd=100, leverage=10, sl=58000, tp=66000,
46
+ client_order_id="sig-2026-09-12-1403") # idempotent: a retry returns the same position
47
+ mp.sltp(pos["id"], trail_pct=1.5) # trailing stop, ratcheted server-side
48
+ print(mp.account())
49
+ ```
50
+
51
+ ## JavaScript
52
+
53
+ ```js
54
+ const { MarginPad } = require('marginpad');
55
+ const mp = new MarginPad('mpb_...');
56
+ const { position } = await mp.open({ symbol: 'BTC', side: 'long', margin_usd: 100, leverage: 10, client_order_id: 'sig-1' });
57
+ mp.stream(ev => console.log(ev.type, ev.data)); // positions pushed on change, prices each tick
58
+ ```
59
+
60
+ ## Examples
61
+
62
+ - `examples/agent_loop.py`: a signal loop that opens, manages and closes positions once a minute without polling storms.
63
+ - `examples/webhook_server.js`: receive `position.closed` / `order.filled` events with signature verification.
64
+ - `examples/mcp.md`: let Claude Desktop or Cursor trade on your paper account through the MCP server.
65
+
66
+ ## Limits
67
+
68
+ Free: 120 requests/minute per key, 3 keys, 50 open positions. Premium: 600/minute, 10 keys, 200 open positions.
69
+ Every response carries `X-RateLimit-Limit / Remaining / Reset`; on 429 both clients wait `Retry-After` and retry once.
70
+ Out-of-range input is refused with a named error (`leverage_max` carries the market's cap), never silently clamped.
71
+
72
+ ## Honesty note
73
+
74
+ This is a simulator. Fees, funding and liquidation math mirror real exchanges; there is no order book, so slippage is modelled, not measured on depth. A bot that passes here still needs a small live test. Educational use, not financial advice.
75
+
76
+ MIT licensed.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ marginpad/__init__.py
4
+ marginpad.egg-info/PKG-INFO
5
+ marginpad.egg-info/SOURCES.txt
6
+ marginpad.egg-info/dependency_links.txt
7
+ marginpad.egg-info/requires.txt
8
+ marginpad.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [stream]
3
+ websockets>=11
@@ -0,0 +1,2 @@
1
+ dist
2
+ marginpad
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "marginpad"
7
+ version = "2.9.0"
8
+ description = "MarginPad Bot API client: free crypto-futures paper trading for bots (REST + WebSocket + webhooks) and free market data. No dependencies."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ authors = [{ name = "MarginPad", email = "support@marginpad.io" }]
13
+ keywords = ["crypto", "futures", "paper-trading", "trading-bot", "backtest", "liquidations", "api", "mcp", "ai-agent"]
14
+ classifiers = ["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Topic :: Office/Business :: Financial :: Investment", "Intended Audience :: Developers"]
15
+
16
+ [project.optional-dependencies]
17
+ stream = ["websockets>=11"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://marginpad.io/trading-api/"
21
+ Documentation = "https://marginpad.io/api-docs/"
22
+ Repository = "https://github.com/marginpad/sdk"
23
+ Changelog = "https://marginpad.io/api/changelog.xml"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+