@2oolkit/pacifica-cli
Advanced tools
+154
-12
@@ -55,2 +55,38 @@ #!/usr/bin/env node | ||
| var BUILDER_MAX_FEE_RATE = "0.001"; | ||
| var CANDLE_MAX_BARS = 4e3; | ||
| var CANDLE_INTERVALS = [ | ||
| "1m", | ||
| "3m", | ||
| "5m", | ||
| "15m", | ||
| "30m", | ||
| "1h", | ||
| "2h", | ||
| "4h", | ||
| "8h", | ||
| "12h", | ||
| "1d" | ||
| ]; | ||
| var INTERVAL_MS = { | ||
| "1m": 6e4, | ||
| "3m": 3 * 6e4, | ||
| "5m": 5 * 6e4, | ||
| "15m": 15 * 6e4, | ||
| "30m": 30 * 6e4, | ||
| "1h": 60 * 6e4, | ||
| "2h": 2 * 60 * 6e4, | ||
| "4h": 4 * 60 * 6e4, | ||
| "8h": 8 * 60 * 6e4, | ||
| "12h": 12 * 60 * 6e4, | ||
| "1d": 24 * 60 * 6e4 | ||
| }; | ||
| function intervalToMs(interval) { | ||
| const ms = INTERVAL_MS[interval]; | ||
| if (ms === void 0) { | ||
| throw new Error( | ||
| `Unknown interval: "${interval}". Valid intervals: ${CANDLE_INTERVALS.join(", ")}` | ||
| ); | ||
| } | ||
| return ms; | ||
| } | ||
| function resolveEnv(input) { | ||
@@ -318,3 +354,8 @@ const normalized = input.toLowerCase().trim(); | ||
| } | ||
| async getCandles(symbol, interval, startTime, endTime) { | ||
| /** | ||
| * Single /kline request. The API caps the time range at {@link CANDLE_MAX_BARS} | ||
| * candles; a wider range returns HTTP 400. `limit` is clamped so an oversized | ||
| * caller-supplied value never produces a raw server error. | ||
| */ | ||
| async getCandles(symbol, interval, startTime, endTime, limit) { | ||
| const params = { | ||
@@ -326,5 +367,47 @@ symbol, | ||
| if (endTime !== void 0) params.end_time = endTime; | ||
| if (limit !== void 0) { | ||
| params.limit = Math.min(Math.max(1, Math.floor(limit)), CANDLE_MAX_BARS); | ||
| } | ||
| const res = await this.http.get("/kline", { params }); | ||
| return res.data; | ||
| } | ||
| /** | ||
| * Fetch up to `maxBars` candles ending at `endTime` by walking the time range | ||
| * backwards in windows of at most {@link CANDLE_MAX_BARS} candles (the API has | ||
| * no cursor — pagination is time-range windowing). Results are deduped by open | ||
| * time, sorted ascending, and trimmed to `maxBars`. | ||
| * | ||
| * @param startTime oldest open time to fetch back to (inclusive) | ||
| * @param endTime newest time to fetch up to (defaults to now) | ||
| * @param maxBars maximum number of candles to return | ||
| */ | ||
| async getCandlesPaginated(symbol, interval, startTime, endTime, maxBars) { | ||
| const intervalMs = intervalToMs(interval); | ||
| const windowSpanMs = (CANDLE_MAX_BARS - 1) * intervalMs; | ||
| const byOpenTime = /* @__PURE__ */ new Map(); | ||
| let windowEnd = endTime ?? Date.now(); | ||
| while (byOpenTime.size < maxBars && windowEnd > startTime) { | ||
| const windowStart = Math.max(startTime, windowEnd - windowSpanMs); | ||
| const res = await this.getCandles( | ||
| symbol, | ||
| interval, | ||
| windowStart, | ||
| windowEnd, | ||
| CANDLE_MAX_BARS | ||
| ); | ||
| const batch = res.data || []; | ||
| if (batch.length === 0) break; | ||
| let oldest = windowEnd; | ||
| for (const candle of batch) { | ||
| byOpenTime.set(candle.t, candle); | ||
| if (candle.t < oldest) oldest = candle.t; | ||
| } | ||
| const nextEnd = oldest - 1; | ||
| if (nextEnd >= windowEnd) break; | ||
| windowEnd = nextEnd; | ||
| } | ||
| const sorted = Array.from(byOpenTime.values()).sort((a, b) => a.t - b.t); | ||
| const trimmed = sorted.length > maxBars ? sorted.slice(sorted.length - maxBars) : sorted; | ||
| return { data: trimmed }; | ||
| } | ||
| async getHistoricalFunding(symbol, limit, cursor) { | ||
@@ -578,2 +661,9 @@ const params = { symbol }; | ||
| } | ||
| function parseIntStrict(value, name) { | ||
| const parsed = parseInt(value, 10); | ||
| if (isNaN(parsed)) { | ||
| throw new Error(`Invalid integer for ${name}: "${value}"`); | ||
| } | ||
| return parsed; | ||
| } | ||
@@ -688,19 +778,71 @@ // src/commands/market.ts | ||
| }); | ||
| market.command("candles").description("Get candlestick data").argument("<symbol>", "Trading symbol").option( | ||
| market.command("candles").description( | ||
| `Get candlestick (OHLCV) data. Up to ${CANDLE_MAX_BARS} bars per API request; --count above that auto-paginates across multiple requests` | ||
| ).argument("<symbol>", "Trading symbol").option( | ||
| "-i, --interval <interval>", | ||
| "Candle interval (1m,5m,15m,1h,4h,1d)", | ||
| `Candle interval (${CANDLE_INTERVALS.join(",")})`, | ||
| "1h" | ||
| ).option( | ||
| "-c, --count <n>", | ||
| `Number of most-recent candles to fetch (auto-paginates above ${CANDLE_MAX_BARS})`, | ||
| "200" | ||
| ).option( | ||
| "--start <timestamp>", | ||
| "Start time in ms", | ||
| String(Date.now() - 24 * 60 * 60 * 1e3) | ||
| ).option("--end <timestamp>", "End time in ms").action(async (symbol, options) => { | ||
| "Start time in ms (overrides --count; clamped to a single request)" | ||
| ).option("--end <timestamp>", "End time in ms (defaults to now)").action(async (symbol, options) => { | ||
| try { | ||
| const client = createPublicClient(); | ||
| const result = await client.getCandles( | ||
| symbol, | ||
| options.interval, | ||
| parseInt(options.start), | ||
| options.end ? parseInt(options.end) : void 0 | ||
| ); | ||
| const intervalMs = intervalToMs(options.interval); | ||
| const endTime = options.end ? parseIntStrict(options.end, "--end") : void 0; | ||
| const maxSpanBars = CANDLE_MAX_BARS - 1; | ||
| let result; | ||
| if (options.start !== void 0) { | ||
| const startTime = parseIntStrict(options.start, "--start"); | ||
| const refEnd = endTime ?? Date.now(); | ||
| const spanBars = Math.ceil((refEnd - startTime) / intervalMs); | ||
| if (spanBars > maxSpanBars) { | ||
| throw new ActionableError( | ||
| `Requested time range spans ~${spanBars} ${options.interval} candles, but a single request is capped at ${CANDLE_MAX_BARS}.`, | ||
| `Use --count <n> to auto-paginate, or narrow the --start/--end range.` | ||
| ); | ||
| } | ||
| result = await client.getCandles( | ||
| symbol, | ||
| options.interval, | ||
| startTime, | ||
| endTime, | ||
| CANDLE_MAX_BARS | ||
| ); | ||
| } else { | ||
| const count = parseIntStrict(options.count, "--count"); | ||
| if (count <= 0) { | ||
| throw new ActionableError( | ||
| `--count must be a positive integer (got ${options.count}).` | ||
| ); | ||
| } | ||
| const refEnd = endTime ?? Date.now(); | ||
| if (count <= CANDLE_MAX_BARS) { | ||
| const startTime = refEnd - Math.min(count, maxSpanBars) * intervalMs; | ||
| const res = await client.getCandles( | ||
| symbol, | ||
| options.interval, | ||
| startTime, | ||
| endTime, | ||
| CANDLE_MAX_BARS | ||
| ); | ||
| const bars = res.data || []; | ||
| result = { | ||
| data: bars.length > count ? bars.slice(bars.length - count) : bars | ||
| }; | ||
| } else { | ||
| const startTime = refEnd - count * intervalMs; | ||
| result = await client.getCandlesPaginated( | ||
| symbol, | ||
| options.interval, | ||
| startTime, | ||
| endTime, | ||
| count | ||
| ); | ||
| } | ||
| } | ||
| const formatted = (result.data || []).map((c) => ({ | ||
@@ -707,0 +849,0 @@ time: formatTimestamp(c.t), |
+118
-7
@@ -46,2 +46,38 @@ #!/usr/bin/env node | ||
| var BUILDER_MAX_FEE_RATE = "0.001"; | ||
| var CANDLE_MAX_BARS = 4e3; | ||
| var CANDLE_INTERVALS = [ | ||
| "1m", | ||
| "3m", | ||
| "5m", | ||
| "15m", | ||
| "30m", | ||
| "1h", | ||
| "2h", | ||
| "4h", | ||
| "8h", | ||
| "12h", | ||
| "1d" | ||
| ]; | ||
| var INTERVAL_MS = { | ||
| "1m": 6e4, | ||
| "3m": 3 * 6e4, | ||
| "5m": 5 * 6e4, | ||
| "15m": 15 * 6e4, | ||
| "30m": 30 * 6e4, | ||
| "1h": 60 * 6e4, | ||
| "2h": 2 * 60 * 6e4, | ||
| "4h": 4 * 60 * 6e4, | ||
| "8h": 8 * 60 * 6e4, | ||
| "12h": 12 * 60 * 6e4, | ||
| "1d": 24 * 60 * 6e4 | ||
| }; | ||
| function intervalToMs(interval) { | ||
| const ms = INTERVAL_MS[interval]; | ||
| if (ms === void 0) { | ||
| throw new Error( | ||
| `Unknown interval: "${interval}". Valid intervals: ${CANDLE_INTERVALS.join(", ")}` | ||
| ); | ||
| } | ||
| return ms; | ||
| } | ||
@@ -144,3 +180,8 @@ // src/signing/signer.ts | ||
| } | ||
| async getCandles(symbol, interval, startTime, endTime) { | ||
| /** | ||
| * Single /kline request. The API caps the time range at {@link CANDLE_MAX_BARS} | ||
| * candles; a wider range returns HTTP 400. `limit` is clamped so an oversized | ||
| * caller-supplied value never produces a raw server error. | ||
| */ | ||
| async getCandles(symbol, interval, startTime, endTime, limit) { | ||
| const params = { | ||
@@ -152,5 +193,47 @@ symbol, | ||
| if (endTime !== void 0) params.end_time = endTime; | ||
| if (limit !== void 0) { | ||
| params.limit = Math.min(Math.max(1, Math.floor(limit)), CANDLE_MAX_BARS); | ||
| } | ||
| const res = await this.http.get("/kline", { params }); | ||
| return res.data; | ||
| } | ||
| /** | ||
| * Fetch up to `maxBars` candles ending at `endTime` by walking the time range | ||
| * backwards in windows of at most {@link CANDLE_MAX_BARS} candles (the API has | ||
| * no cursor — pagination is time-range windowing). Results are deduped by open | ||
| * time, sorted ascending, and trimmed to `maxBars`. | ||
| * | ||
| * @param startTime oldest open time to fetch back to (inclusive) | ||
| * @param endTime newest time to fetch up to (defaults to now) | ||
| * @param maxBars maximum number of candles to return | ||
| */ | ||
| async getCandlesPaginated(symbol, interval, startTime, endTime, maxBars) { | ||
| const intervalMs = intervalToMs(interval); | ||
| const windowSpanMs = (CANDLE_MAX_BARS - 1) * intervalMs; | ||
| const byOpenTime = /* @__PURE__ */ new Map(); | ||
| let windowEnd = endTime ?? Date.now(); | ||
| while (byOpenTime.size < maxBars && windowEnd > startTime) { | ||
| const windowStart = Math.max(startTime, windowEnd - windowSpanMs); | ||
| const res = await this.getCandles( | ||
| symbol, | ||
| interval, | ||
| windowStart, | ||
| windowEnd, | ||
| CANDLE_MAX_BARS | ||
| ); | ||
| const batch = res.data || []; | ||
| if (batch.length === 0) break; | ||
| let oldest = windowEnd; | ||
| for (const candle of batch) { | ||
| byOpenTime.set(candle.t, candle); | ||
| if (candle.t < oldest) oldest = candle.t; | ||
| } | ||
| const nextEnd = oldest - 1; | ||
| if (nextEnd >= windowEnd) break; | ||
| windowEnd = nextEnd; | ||
| } | ||
| const sorted = Array.from(byOpenTime.values()).sort((a, b) => a.t - b.t); | ||
| const trimmed = sorted.length > maxBars ? sorted.slice(sorted.length - maxBars) : sorted; | ||
| return { data: trimmed }; | ||
| } | ||
| async getHistoricalFunding(symbol, limit, cursor) { | ||
@@ -366,11 +449,39 @@ const params = { symbol }; | ||
| "get_candles", | ||
| "Get candlestick data for a symbol", | ||
| `Get candlestick (OHLCV) data for a symbol. A single API request returns up to ${CANDLE_MAX_BARS} candles; pass "count" above that to auto-paginate across requests.`, | ||
| { | ||
| symbol: import_zod.z.string(), | ||
| interval: import_zod.z.string().describe("1m,5m,15m,1h,4h,1d"), | ||
| start_time: import_zod.z.number().describe("Start time in ms"), | ||
| end_time: import_zod.z.number().optional().describe("End time in ms") | ||
| interval: import_zod.z.enum(CANDLE_INTERVALS).describe(CANDLE_INTERVALS.join(",")), | ||
| count: import_zod.z.number().optional().describe( | ||
| `Number of most-recent candles to fetch (auto-paginates above ${CANDLE_MAX_BARS}). Ignored when start_time is given.` | ||
| ), | ||
| start_time: import_zod.z.number().optional().describe("Start time in ms (overrides count; single request)"), | ||
| end_time: import_zod.z.number().optional().describe("End time in ms (defaults to now)") | ||
| }, | ||
| async ({ symbol, interval, start_time, end_time }) => { | ||
| return withErrorHandling(() => createPublicClient().getCandles(symbol, interval, start_time, end_time)); | ||
| async ({ symbol, interval, count, start_time, end_time }) => { | ||
| return withErrorHandling(async () => { | ||
| const client = createPublicClient(); | ||
| if (start_time !== void 0) { | ||
| return client.getCandles(symbol, interval, start_time, end_time, CANDLE_MAX_BARS); | ||
| } | ||
| const n = count ?? 200; | ||
| const intervalMs = intervalToMs(interval); | ||
| const refEnd = end_time ?? Date.now(); | ||
| const maxSpanBars = CANDLE_MAX_BARS - 1; | ||
| if (n <= CANDLE_MAX_BARS) { | ||
| const startTime2 = refEnd - Math.min(n, maxSpanBars) * intervalMs; | ||
| const res = await client.getCandles( | ||
| symbol, | ||
| interval, | ||
| startTime2, | ||
| end_time, | ||
| CANDLE_MAX_BARS | ||
| ); | ||
| const bars = res.data || []; | ||
| return { | ||
| data: bars.length > n ? bars.slice(bars.length - n) : bars | ||
| }; | ||
| } | ||
| const startTime = refEnd - n * intervalMs; | ||
| return client.getCandlesPaginated(symbol, interval, startTime, end_time, n); | ||
| }); | ||
| } | ||
@@ -377,0 +488,0 @@ ); |
+2
-2
| { | ||
| "name": "@2oolkit/pacifica-cli", | ||
| "version": "0.1.2", | ||
| "version": "0.1.3", | ||
| "description": "CLI toolkit & MCP server for trading perpetual futures on Pacifica exchange — crypto, forex, commodities, equities on Solana", | ||
@@ -68,3 +68,3 @@ "homepage": "https://github.com/haeminmoon/pacifica-cli#readme", | ||
| "tweetnacl": "^1.0.3", | ||
| "zod": "^3.24.0" | ||
| "zod": "^4.4.3" | ||
| }, | ||
@@ -71,0 +71,0 @@ "devDependencies": { |
+10
-1
@@ -85,6 +85,15 @@ # @2oolkit/pacifica-cli | ||
| pacifica-cli market trades ETH # Recent trades | ||
| pacifica-cli market candles BTC -i 4h # Candlestick data | ||
| pacifica-cli market candles BTC -i 4h # Candlestick data (default 200 bars) | ||
| pacifica-cli market candles BTC -i 1m -c 4000 # Up to 4000 bars in one request (per-request max) | ||
| pacifica-cli market candles BTC -i 1m -c 8000 # >4000 auto-paginates across requests | ||
| pacifica-cli market funding SOL -l 50 # Funding rate history | ||
| ``` | ||
| **Candles (`market candles`):** The Pacifica `/kline` API returns at most **4000 bars per | ||
| request**. Use `-c, --count <n>` to fetch the N most recent bars (default `200`); when `n` exceeds | ||
| 4000 the CLI auto-paginates by windowing the time range backwards, deduping and sorting ascending | ||
| by time, and returns up to `n` bars. Intervals: `1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d`. | ||
| For an explicit window use `--start <ms>` (and optional `--end <ms>`); a range wider than the | ||
| 4000-bar cap is rejected with a clear message — use `--count` to page through more. | ||
| #### Orders (auth required) | ||
@@ -91,0 +100,0 @@ |
@@ -129,6 +129,11 @@ # Market Data Reference | ||
| | `1m` | 1 minute | | ||
| | `3m` | 3 minutes | | ||
| | `5m` | 5 minutes | | ||
| | `15m` | 15 minutes | | ||
| | `30m` | 30 minutes | | ||
| | `1h` | 1 hour | | ||
| | `2h` | 2 hours | | ||
| | `4h` | 4 hours | | ||
| | `8h` | 8 hours | | ||
| | `12h` | 12 hours | | ||
| | `1d` | 1 day | | ||
@@ -148,9 +153,26 @@ | ||
| ### Number of Bars & Pagination | ||
| The Pacifica `/kline` API returns **at most 4000 bars per request**. The CLI exposes this through | ||
| `-c, --count <n>` (default `200`), which fetches the N most-recent bars ending now (or at `--end`): | ||
| ```bash | ||
| # Default: 200 most-recent bars | ||
| pacifica-cli market candles BTC -i 1m | ||
| # Up to the per-request maximum (single request; may be slightly fewer due to market gaps) | ||
| pacifica-cli market candles BTC -i 1m -c 4000 -o json | ||
| # More than 4000: the CLI AUTO-PAGINATES, windowing the time range backwards in | ||
| # <=4000-bar chunks, deduping and sorting ascending by time, capped at the requested count | ||
| pacifica-cli market candles BTC -i 1m -c 8000 -o json | jq 'length' # ~8000 | ||
| ``` | ||
| When `--count` exceeds 4000 the CLI issues multiple `/kline` requests automatically — no manual | ||
| paging needed. Bars are returned oldest-first with no duplicates, and never more than `--count`. | ||
| ### Custom Time Range | ||
| ```bash | ||
| # Last 24 hours (default) | ||
| pacifica-cli market candles BTC -i 1h | ||
| # Custom start time (milliseconds) | ||
| # Custom start time (milliseconds). Overrides --count; a single request only. | ||
| pacifica-cli market candles BTC -i 1h --start 1700000000000 | ||
@@ -162,2 +184,5 @@ | ||
| A `--start`/`--end` range wider than 4000 bars is rejected with a clear message (it does not leak a | ||
| raw server error). To pull more than 4000 bars, use `--count <n>` so the CLI auto-paginates. | ||
| ## Funding Rates | ||
@@ -164,0 +189,0 @@ |
+21
-2
@@ -140,6 +140,25 @@ --- | ||
| | `pacifica-cli market trades ETH` | Recent trades | | ||
| | `pacifica-cli market candles BTC -i 4h` | Candlestick data | | ||
| | `pacifica-cli market candles BTC -i 1h --start <ms>` | With custom start time | | ||
| | `pacifica-cli market candles BTC -i 4h` | Candlestick data (default 200 bars) | | ||
| | `pacifica-cli market candles BTC -i 1m -c 4000` | Up to 4000 bars in one request (per-request max) | | ||
| | `pacifica-cli market candles BTC -i 1m -c 8000` | More than 4000 bars (auto-paginates) | | ||
| | `pacifica-cli market candles BTC -i 1h --start <ms>` | Explicit window (single request, <=4000 bars) | | ||
| | `pacifica-cli market funding SOL -l 50` | Funding rate history | | ||
| #### Candlestick Data (intervals & pagination) | ||
| Supported intervals: `1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d`. | ||
| The `/kline` API returns **at most 4000 bars per request**. Use `-c, --count <n>` (default `200`) to | ||
| fetch the N most-recent bars. When `n` exceeds 4000, the CLI auto-paginates — it windows the time | ||
| range backwards in `<=4000`-bar chunks, dedupes, sorts ascending by time, and returns up to `n` bars. | ||
| ```bash | ||
| pacifica-cli market candles BTC -i 1m -c 4000 -o json | jq 'length' # ~4000 (one request) | ||
| pacifica-cli market candles BTC -i 1m -c 8000 -o json | jq 'length' # ~8000 (auto-paginated) | ||
| ``` | ||
| `--start <ms>` (with optional `--end <ms>`) requests an explicit window in a single request; a range | ||
| wider than 4000 bars is rejected with a clear message rather than a raw server error — use `--count` | ||
| to page through more. | ||
| ### Trading (Authentication Required) | ||
@@ -146,0 +165,0 @@ |
109100
12%1804
16.31%306
3.03%+ Added
- Removed
Updated