marginpad 2.9.0__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.
marginpad/__init__.py
ADDED
|
@@ -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,5 @@
|
|
|
1
|
+
marginpad/__init__.py,sha256=vDwBfgirPmQnu1tNaMYAJOtjYDrN147eCwpDDmOnBn4,15865
|
|
2
|
+
marginpad-2.9.0.dist-info/METADATA,sha256=WEUR7Jj0zL6P8KO9aoKDfmaTzf4n35jLAiNYH0BSS1Y,3701
|
|
3
|
+
marginpad-2.9.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
4
|
+
marginpad-2.9.0.dist-info/top_level.txt,sha256=UXieKh8J9Rj7057z5RbZ3UDOKhrSpMQUBItd-qqzjNQ,10
|
|
5
|
+
marginpad-2.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
marginpad
|