marginpad 2.9.1

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.
Files changed (4) hide show
  1. package/README.md +56 -0
  2. package/index.d.ts +27 -0
  3. package/index.js +153 -0
  4. package/package.json +14 -0
package/README.md ADDED
@@ -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.
package/index.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export interface Position { id: string; symbol: string; side: 'long' | 'short'; entry_price: number; margin_usd: number; leverage: number; qty: number; liq_price: number; sl: number | null; tp: number | null; status: 'open' | 'closed' | 'liquidated'; opened_ts: number; mark_price?: number; unrealized_pnl_usd?: number; exit_price?: number | null; pnl_usd?: number | null; closed_ts?: number | null; trail_pct?: number; fee_rate_pct?: number; fee_venue?: string | null; }
2
+ export interface OpenParams { symbol: string; side: 'long' | 'short'; margin_usd: number; leverage?: number; sl?: number; tp?: number; trail_pct?: number; type?: 'market' | 'limit' | 'stop'; limit_price?: number; client_order_id?: string; fee_venue?: string | null; dry_run?: boolean; }
3
+ export class MarginPadError extends Error { code: string; status: number; body: any; }
4
+ export class MarginPad {
5
+ constructor(key?: string, opts?: { baseUrl?: string; autoRetry?: boolean; timeoutMs?: number });
6
+ price(symbol: string): Promise<{ symbol: string; price: number; change_24h_pct: number | null; ts: number }>;
7
+ klines(symbol: string, interval?: string, end?: number): Promise<any[]>;
8
+ time(): Promise<{ server_time_ms: number; server_time_iso: string }>;
9
+ open(p: OpenParams): Promise<{ position?: Position; order?: any; dry_run?: boolean }>;
10
+ close(id: string, opts?: { pct?: number; symbol?: string; client_order_id?: string }): Promise<{ position: Position }>;
11
+ closeAll(): Promise<{ closed: number; positions: Position[] }>;
12
+ sltp(id: string, p: { sl?: number | null; tp?: number | null; trail_pct?: number | null }): Promise<{ position: Position }>;
13
+ positions(): Promise<{ positions: Position[] }>;
14
+ orders(): Promise<{ orders: any[] }>;
15
+ cancelOrder(id: string): Promise<{ order_id: string; status: string }>;
16
+ modifyOrder(id: string, p: any): Promise<any>;
17
+ trades(opts?: { limit?: number; before?: number }): Promise<{ trades: Position[]; next_before: number | null }>;
18
+ account(): Promise<any>;
19
+ balance(): Promise<any>;
20
+ markets(): Promise<any>;
21
+ fees(): Promise<any>;
22
+ report(days?: number): Promise<any>;
23
+ usage(): Promise<any>;
24
+ webhooks(): Promise<any>;
25
+ stream(onEvent: (ev: { type: string; data: any }) => void, opts?: any): { close(): void };
26
+ }
27
+ export function verifyWebhook(secret: string, rawBody: string, signatureHeader: string): boolean;
package/index.js ADDED
@@ -0,0 +1,153 @@
1
+ /* MarginPad Bot API client - one file, zero dependencies. Node 18+ (global fetch) or any browser.
2
+ *
3
+ * const { MarginPad } = require('./marginpad.js'); // or: import { MarginPad } from './marginpad.js'
4
+ * const mp = new MarginPad('mpb_...'); // key from https://marginpad.io/trading-api/
5
+ * console.log(await mp.price('BTC')); // keyless market data
6
+ * const { position } = await mp.open({ symbol: 'BTC', side: 'long', margin_usd: 100, leverage: 10,
7
+ * sl: 58000, tp: 66000, client_order_id: 'sig-2026-09-11-1403' });
8
+ * await mp.sltp(position.id, { trail_pct: 1.5 }); // trailing stop, ratcheted server-side
9
+ * mp.stream(ev => console.log(ev.type, ev.data)); // WebSocket push (Node 22+ or a browser; else npm i ws)
10
+ *
11
+ * Every method resolves with the `data` part of the v2 envelope and rejects with MarginPadError on {ok:false}.
12
+ * On 429 the client waits Retry-After and retries ONCE (autoRetry, default on).
13
+ * Docs: https://marginpad.io/trading-api/ · Spec: https://marginpad.io/api/openapi.json
14
+ */
15
+ (function (root, factory) {
16
+ if (typeof module === 'object' && module.exports) module.exports = factory();
17
+ else root.MarginPadSDK = factory();
18
+ })(typeof self !== 'undefined' ? self : this, function () {
19
+ 'use strict';
20
+ var VERSION = '2.9.1';
21
+
22
+ function MarginPadError(code, message, status, extra) {
23
+ var e = new Error(code + ': ' + (message || code));
24
+ e.name = 'MarginPadError'; e.code = code; e.message = message || code; e.status = status || 0; e.extra = extra || {};
25
+ return e;
26
+ }
27
+
28
+ function MarginPad(apiKey, opts) {
29
+ opts = opts || {};
30
+ this.apiKey = apiKey || null;
31
+ this.base = (opts.base || 'https://marginpad.io').replace(/\/+$/, '');
32
+ this.autoRetry = opts.autoRetry !== false;
33
+ this.timeout = opts.timeout || 15000;
34
+ this.lastHeaders = {};
35
+ this._etag = null;
36
+ }
37
+
38
+ MarginPad.prototype._call = async function (method, path, params, body, keyed, headers, retried) {
39
+ var url = this.base + path;
40
+ if (params) { var q = Object.keys(params).filter(function (k) { return params[k] != null; }).map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&'); if (q) url += (url.indexOf('?') > 0 ? '&' : '?') + q; }
41
+ var h = { accept: 'application/json', 'user-agent': 'marginpad-js/' + VERSION };
42
+ if (keyed !== false && this.apiKey) h['x-api-key'] = this.apiKey;
43
+ if (body != null) h['content-type'] = 'application/json';
44
+ if (headers) for (var k in headers) if (headers[k] != null) h[k] = headers[k];
45
+ var ctl = typeof AbortController !== 'undefined' ? new AbortController() : null, to = ctl ? setTimeout(function () { ctl.abort(); }, this.timeout) : null;
46
+ var r;
47
+ try { r = await fetch(url, { method: method, headers: h, body: body != null ? JSON.stringify(body) : undefined, signal: ctl ? ctl.signal : undefined }); }
48
+ finally { if (to) clearTimeout(to); }
49
+ var lh = {}; try { r.headers.forEach(function (v, k) { lh[k] = v; }); } catch (e) {}
50
+ this.lastHeaders = lh;
51
+ if (r.status === 304) return null;
52
+ if (r.status === 429 && this.autoRetry && !retried) { var ra = parseInt(r.headers.get('retry-after') || '5', 10); await new Promise(function (res) { setTimeout(res, Math.min(60000, Math.max(1000, (isNaN(ra) ? 5 : ra) * 1000))); }); return this._call(method, path, params, body, keyed, headers, true); }
53
+ var text = await r.text(); var j = null; try { j = JSON.parse(text); } catch (e) { j = null; }
54
+ if (!r.ok) { var err = j && j.error; if (err && typeof err === 'object') throw MarginPadError(err.code || 'error', err.message, r.status, err); throw MarginPadError(String(err || ('http_' + r.status)), (j && j.hint) || text.slice(0, 200), r.status, j || {}); }
55
+ if (j && j.ok === false) { var e2 = j.error || {}; throw MarginPadError(e2.code || 'error', e2.message, r.status, e2); }
56
+ return j && Object.prototype.hasOwnProperty.call(j, 'data') ? j.data : j;
57
+ };
58
+ MarginPad.prototype._get = function (path, params, keyed) { return this._call('GET', path, params, null, keyed); };
59
+ MarginPad.prototype._post = function (path, body) { var b = {}; for (var k in (body || {})) if (body[k] !== undefined) b[k] = body[k]; return this._call('POST', path, null, b); };
60
+
61
+ // ── market data (keyless; with a key it counts against your per-key budget instead of the per-IP one) ──
62
+ MarginPad.prototype.price = function (symbol) { return this._get('/api/v1/price', { symbol: symbol }); };
63
+ MarginPad.prototype.prices = function () { return this._get('/api/v1/prices'); };
64
+ MarginPad.prototype.klines = function (symbol, interval, endMs) { return this._get('/api/v1/klines', { symbol: symbol, interval: interval || 60, end: endMs }); };
65
+ MarginPad.prototype.markets = function (assetClass) { return this._get('/api/bot/v2/markets', { 'class': assetClass }); };
66
+ MarginPad.prototype.screener = function () { return this._get('/api/v1/screener'); };
67
+ MarginPad.prototype.funding = function () { return this._get('/api/v1/funding'); };
68
+ MarginPad.prototype.openInterest = function () { return this._get('/api/v1/open-interest'); };
69
+ MarginPad.prototype.liquidations = function () { return this._get('/api/v1/liquidations'); };
70
+ MarginPad.prototype.fearGreed = function () { return this._get('/api/v1/fear-greed'); };
71
+ MarginPad.prototype.calendar = function (year) { return this._get('/api/v1/calendar', { year: year }); };
72
+ MarginPad.prototype.serverTime = function () { return this._get('/api/bot/v2/time', { client_ts: Date.now() }, false); };
73
+
74
+ // ── paper trading ────────────────────────────────────────────────────────────────────────────────────
75
+ // open({symbol, side, margin_usd, leverage, sl, tp, trail_pct, client_order_id, dry_run})
76
+ MarginPad.prototype.open = function (o) { return this._post('/api/bot/v2/open', o); };
77
+ // limitOrder({symbol, side, limit_price, ...}) = pullback entry; stopOrder({..., stop_price}) = breakout entry
78
+ MarginPad.prototype.limitOrder = function (o) { return this._post('/api/bot/v2/open', Object.assign({ type: 'limit' }, o)); };
79
+ MarginPad.prototype.stopOrder = function (o) { var b = Object.assign({ type: 'stop' }, o); if (b.stop_price != null && b.limit_price == null) { b.limit_price = b.stop_price; delete b.stop_price; } return this._post('/api/bot/v2/open', b); };
80
+ MarginPad.prototype.orders = function () { return this._get('/api/bot/v2/orders'); };
81
+ MarginPad.prototype.modifyOrder = function (orderId, changes) { return this._post('/api/bot/v2/modify_order', Object.assign({ order_id: orderId }, changes || {})); };
82
+ MarginPad.prototype.cancelOrder = function (orderId) { return this._post('/api/bot/v2/cancel_order', { order_id: orderId }); };
83
+ // close(id, {symbol, pct, client_order_id}) - pass symbol: one price fetch, one round trip
84
+ MarginPad.prototype.close = function (id, o) { return this._post('/api/bot/v2/close', Object.assign({ id: id }, o || {})); };
85
+ MarginPad.prototype.closeAll = function () { return this._post('/api/bot/v2/close_all', {}); };
86
+ // sltp(id, {sl, tp, trail_pct}) - null clears a level, an omitted key keeps it
87
+ MarginPad.prototype.sltp = function (id, o) { return this._post('/api/bot/v2/sltp', Object.assign({ id: id }, o || {})); };
88
+ // positions({status:'open'|'closed', since}) - resolves null when nothing changed (ETag / 304)
89
+ MarginPad.prototype.positions = async function (o) { o = o || {}; var d = await this._call('GET', '/api/bot/v2/positions', { status: o.status, since: o.since }, null, true, this._etag && o.useEtag !== false ? { 'if-none-match': this._etag } : null); var et = this.lastHeaders.etag; if (et) this._etag = et; return d; };
90
+ MarginPad.prototype.trades = function (limit, beforeMs) { return this._get('/api/bot/v2/trades', { limit: limit || 100, before: beforeMs }); };
91
+ MarginPad.prototype.account = function () { return this._get('/api/bot/v2/account'); };
92
+ MarginPad.prototype.balance = function () { return this._get('/api/bot/v2/balance'); };
93
+ MarginPad.prototype.usage = function () { return this._get('/api/bot/v2/usage'); };
94
+ MarginPad.prototype.report = function (days) { return this._get('/api/bot/v2/report', { days: days || 30 }); };
95
+ // 2.5 books, reset, equity · 2.6 replay
96
+ MarginPad.prototype.accounts = function () { return this._get('/api/bot/v2/accounts'); };
97
+ MarginPad.prototype.reset = function () { return this._post('/api/bot/v2/reset', { confirm: true }); };
98
+ MarginPad.prototype.equity = function (days, stepMin) { return this._get('/api/bot/v2/equity', { days: days || 30, step_min: stepMin }); };
99
+ MarginPad.prototype.replayStart = function (o) { return this._post('/api/bot/v2/replay', { symbol: o.symbol, day: o.day, speed: o.speed }); };
100
+ MarginPad.prototype.replay = function (o) { o = o || {}; return this._get('/api/bot/v2/replay', { interval: o.interval, bars: o.bars }); };
101
+ MarginPad.prototype.replayStop = function () { return this._post('/api/bot/v2/replay', { act: 'stop' }); };
102
+ // ai({symbol, interval, question, lang}) - Premium; educational, not advice
103
+ MarginPad.prototype.ai = function (o) { return this._post('/api/bot/v2/ai', Object.assign({}, o, { interval: String((o && o.interval) || 60) })); };
104
+
105
+ // ── fee schedule and fill realism (2.9) ──────────────────────────────────
106
+ // fees() reads the table and your default; fees('binance') sets it; fees('') goes back to ours.
107
+ MarginPad.prototype.fees = function (venue) { return venue === undefined ? this._get('/api/bot/v2/fees') : this._post('/api/bot/v2/fees', { venue: venue }); };
108
+
109
+ // Fill realism. Both switches are OFF by default: a market order fills at the live price and maintenance
110
+ // margin is a flat 0.5%, which is easier than a real venue and increasingly so as a position grows.
111
+ // await c.realism() -> your setting, every venue rate, the tier ladder
112
+ // await c.realism({ slippage: true, margin_tiers: true }) -> test against something closer to a real book
113
+ // await c.realism({ margin_venue: 'binance' }) -> liquidate where Binance would
114
+ // Pass the same fields to open() to override the account for one fill only.
115
+ MarginPad.prototype.realism = function (o) { return o === undefined ? this._get('/api/bot/v2/realism') : this._post('/api/bot/v2/realism', o); };
116
+
117
+ // ── webhooks (API Pro and up) ───────────────────────────────────────────────────────────────────────────────
118
+ MarginPad.prototype.webhooks = function () { return this._get('/api/bot/v2/webhooks'); };
119
+ MarginPad.prototype.addWebhook = function (url, events) { return this._post('/api/bot/v2/webhooks', { act: 'add', url: url, events: events }); };
120
+ MarginPad.prototype.deleteWebhook = function (id) { return this._post('/api/bot/v2/webhooks', { act: 'delete', id: id }); };
121
+ MarginPad.prototype.testWebhook = function (id) { return this._post('/api/bot/v2/webhooks', { act: 'test', id: id }); };
122
+
123
+ // ── WebSocket stream ─────────────────────────────────────────────────────────────────────────────────
124
+ // stream(onEvent, {channels:['positions','prices'], reconnect:true}) -> returns { close() }
125
+ MarginPad.prototype.stream = function (onEvent, o) {
126
+ o = o || {}; var self = this, stopped = false, ws = null;
127
+ var WS = (typeof WebSocket !== 'undefined') ? WebSocket : null;
128
+ if (!WS && typeof require === 'function') { try { WS = require('ws'); } catch (e) { WS = null; } }
129
+ if (!WS) throw new Error('No WebSocket available: use Node 22+, a browser, or npm i ws');
130
+ var url = self.base.replace(/^http/, 'ws') + '/api/bot/v2/stream?api_key=' + encodeURIComponent(self.apiKey || '');
131
+ function connect() {
132
+ if (stopped) return;
133
+ ws = new WS(url);
134
+ ws.onopen = function () { try { ws.send(JSON.stringify({ op: 'subscribe', channels: o.channels || ['positions', 'prices'] })); } catch (e) {} };
135
+ ws.onmessage = function (m) { var d = null; try { d = JSON.parse(typeof m.data === 'string' ? m.data : String(m.data)); } catch (e) { return; } try { onEvent(d); } catch (e) {} };
136
+ ws.onclose = function () { if (!stopped && o.reconnect !== false) setTimeout(connect, 3000); };
137
+ ws.onerror = function () { try { ws.close(); } catch (e) {} };
138
+ }
139
+ connect();
140
+ var ping = setInterval(function () { try { if (ws && ws.readyState === 1) ws.send(JSON.stringify({ op: 'ping' })); } catch (e) {} }, 25000);
141
+ return { close: function () { stopped = true; clearInterval(ping); try { ws && ws.close(); } catch (e) {} } };
142
+ };
143
+
144
+ // ── webhook receiver helper: verify X-MP-Signature over the RAW body before parsing it ───────────────
145
+ async function verifyWebhook(secret, rawBody, signatureHeader) {
146
+ var want;
147
+ if (typeof require === 'function') { try { var c = require('crypto'); want = 'sha256=' + c.createHmac('sha256', secret).update(rawBody).digest('hex'); } catch (e) { want = null; } }
148
+ if (!want) { var enc = new TextEncoder(); var key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); var sig = await crypto.subtle.sign('HMAC', key, typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody); want = 'sha256=' + Array.prototype.map.call(new Uint8Array(sig), function (x) { return ('0' + x.toString(16)).slice(-2); }).join(''); }
149
+ return want === String(signatureHeader || '');
150
+ }
151
+
152
+ return { MarginPad: MarginPad, MarginPadError: MarginPadError, verifyWebhook: verifyWebhook, VERSION: VERSION };
153
+ });
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "marginpad",
3
+ "version": "2.9.1",
4
+ "description": "MarginPad Bot API client: free crypto-futures paper trading for bots (REST + WebSocket + webhooks) and free market data. Zero dependencies.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "license": "MIT",
8
+ "repository": { "type": "git", "url": "https://github.com/marginpad/sdk" },
9
+ "homepage": "https://marginpad.io/trading-api/",
10
+ "bugs": { "url": "https://github.com/marginpad/sdk/issues" },
11
+ "keywords": ["crypto", "futures", "paper-trading", "trading-bot", "backtest", "liquidations", "bybit", "binance", "api", "mcp", "ai-agent"],
12
+ "engines": { "node": ">=18" },
13
+ "files": ["index.js", "index.d.ts", "README.md"]
14
+ }