diff --git a/AGENTS.md b/AGENTS.md index b634dae..2610607 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -306,7 +306,7 @@ Spot paper and futures paper are fully independent. Resetting one does not affec | auth | No | 4 | Credential management (set, show, test, reset) | | utility | No | 2 | Interactive setup and REPL shell | -Total: 151 commands. For the full machine-readable catalog, see `agents/tool-catalog.json`. +Total: 152 commands. For the full machine-readable catalog, see `agents/tool-catalog.json`. `kraken mcp` is a runtime mode that starts an MCP server, not a tool-callable command. It is not included in the catalog. @@ -384,7 +384,7 @@ import json with open("agents/tool-catalog.json") as f: catalog = json.load(f) -# All 151 commands with full parameter schemas +# All 152 commands with full parameter schemas commands = catalog["commands"] # Filter to safe read-only commands @@ -462,7 +462,7 @@ Configure your MCP client: | File | Format | Description | |------|--------|-------------| -| `agents/tool-catalog.json` | JSON | All 151 commands with parameters, types, safety flags, and examples | +| `agents/tool-catalog.json` | JSON | All 152 commands with parameters, types, safety flags, and examples | | `agents/error-catalog.json` | JSON | 9 error categories with retry guidance | | `agents/examples/` | Shell | Runnable workflow examples | | `skills/` | SKILL.md | Goal-oriented workflow skills for agents | diff --git a/CLAUDE.md b/CLAUDE.md index b06a768..c792a2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,5 +132,5 @@ Load `agents/tool-catalog.json` for the full machine-readable command contract. - `AGENTS.md`: Complete agent integration guide - `CONTEXT.md`: Runtime-optimized context for tool-using agents -- `agents/tool-catalog.json`: All 151 commands with parameters +- `agents/tool-catalog.json`: All 152 commands with parameters - `agents/error-catalog.json`: Error categories with retry guidance diff --git a/CONTEXT.md b/CONTEXT.md index aa9f44a..743e8c8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -151,5 +151,5 @@ Security note: ## Tool Discovery Use these machine-readable files: -- `agents/tool-catalog.json`: full command catalog (151 commands with parameter schemas and `dangerous` flags) +- `agents/tool-catalog.json`: full command catalog (152 commands with parameter schemas and `dangerous` flags) - `agents/error-catalog.json`: error categories and retry policy diff --git a/README.md b/README.md index 0df1265..50a5f45 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ If you're an AI agent or building one, start here: |----------|-------------| | [CONTEXT.md](CONTEXT.md) | Runtime context for tool-using agents | | [AGENTS.md](AGENTS.md) | Full integration guide: auth, invocation, errors, rate limits | -| [agents/tool-catalog.json](agents/tool-catalog.json) | 151 commands with parameter schemas, types, and safety flags | +| [agents/tool-catalog.json](agents/tool-catalog.json) | 152 commands with parameter schemas, types, and safety flags | | [agents/error-catalog.json](agents/error-catalog.json) | 9 error categories with retry guidance | | [skills/](skills/) | 50 goal-oriented SKILL.md workflow packages | | [CLAUDE.md](CLAUDE.md) | Claude-specific integration guidance | @@ -125,7 +125,7 @@ Most CLIs are built for humans at a terminal. This one is built for LLM-based ag - **Consistent error envelopes.** Errors are JSON objects with a stable `error` field (`auth`, `rate_limit`, `validation`, `api`, `network`). Agents route on `error` without parsing human sentences. - **Predictable exit codes.** Success is 0, failure is non-zero. Combined with JSON errors on stdout, agents detect and classify failures programmatically. - **Paper trading for safe iteration.** Test strategies against live prices with `kraken paper` (spot) and `kraken futures paper` (perpetual futures) commands. No API keys, no real money, same interface as live trading. -- **Full API surface.** 151 commands covering Spot, Futures, xStocks, Forex, Funding, Earn, Subaccounts, WebSocket streaming, and paper trading for both spot and futures. +- **Full API surface.** 152 commands covering Spot, Futures, xStocks, Forex, Funding, Earn, Subaccounts, WebSocket streaming, and paper trading for both spot and futures. - **Built-in MCP server.** Native Model Context Protocol support over stdio. No subprocess wrappers needed. - **Rate-limit aware.** No client-side throttling. When the Kraken API rejects a request due to rate limits, the CLI returns an enriched error with `suggestion`, `retryable`, and `docs_url` fields so agents can read the documentation and adapt their strategy. @@ -362,7 +362,7 @@ kraken balance -o json -v 2>/dev/null | jq . ## Commands -151 commands across 13 groups. For machine-readable parameter schemas, load [agents/tool-catalog.json](agents/tool-catalog.json). +152 commands across 13 groups. For machine-readable parameter schemas, load [agents/tool-catalog.json](agents/tool-catalog.json). | Group | Commands | Auth | Description | |-------|----------|------|-------------| @@ -481,6 +481,7 @@ kraken balance -o json -v 2>/dev/null | jq . | `kraken futures ticker ` | Single ticker | | `kraken futures orderbook ` | Order book | | `kraken futures history [--since TS] [--before TS]` | Trade history | +| `kraken futures ohlc [--interval 1d] [--tick-type trade] [--from TS] [--to TS]` | OHLC candles (timestamps in ms) | | `kraken futures feeschedules` | Fee schedules | | `kraken futures instrument-status [--symbol SYM]` | Instrument status | diff --git a/agents/tool-catalog.json b/agents/tool-catalog.json index 5c540e6..20c4a5e 100644 --- a/agents/tool-catalog.json +++ b/agents/tool-catalog.json @@ -2671,6 +2671,48 @@ ], "example": "kraken futures historical-funding-rates PI_XBTUSD" }, + { + "name": "futures-ohlc", + "command": "kraken futures ohlc ", + "group": "futures", + "description": "Get OHLC candle data for a futures contract. Candle timestamps are in milliseconds since epoch (the spot OHLC endpoint uses seconds).", + "auth_required": false, + "dangerous": false, + "parameters": [ + { + "name": "symbol", + "type": "string", + "required": true, + "positional": true, + "description": "Futures symbol (e.g. PF_XBTUSD)" + }, + { + "name": "--interval", + "type": "string", + "required": false, + "description": "Candle resolution: 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d, 1w (default 1d)" + }, + { + "name": "--tick-type", + "type": "string", + "required": false, + "description": "Tick type for the price series: trade, mark, index (default trade)" + }, + { + "name": "--from", + "type": "integer", + "required": false, + "description": "Start of range, unix seconds (inclusive)" + }, + { + "name": "--to", + "type": "integer", + "required": false, + "description": "End of range, unix seconds (inclusive)" + } + ], + "example": "kraken futures ohlc PF_XBTUSD --interval 1d --from 1735689600 --to 1735862400 -o json" + }, { "name": "futures-order-status", "command": "kraken futures order-status", diff --git a/src/client.rs b/src/client.rs index db232d9..d2800a6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -514,6 +514,59 @@ impl FuturesClient { } } + /// Execute a public GET request on the Futures charts API, with retry on + /// transient errors and 5xx server errors (GET is inherently idempotent). + /// + /// No auth headers. + pub async fn public_get_charts( + &self, + tick_type: &str, + symbol: &str, + resolution: &str, + params: &[(&str, &str)], + verbose: bool, + ) -> Result { + let mut base_url = url::Url::parse(&self.base_url) + .map_err(|e| KrakenError::Validation(format!("Invalid futures base URL: {e}")))?; + // The charts API lives at a different path prefix than the v3 derivatives + base_url.set_path(&format!("/api/charts/v1/{tick_type}/{symbol}/{resolution}")); + let url = base_url.to_string(); + + if verbose { + crate::output::verbose(&format!("GET {url}")); + } + + let mut attempt = 0u32; + loop { + let response = self.http.get(&url).query(params).send().await; + match response { + Ok(r) if r.status().is_server_error() && attempt < MAX_RETRIES => { + let status = r.status(); + attempt += 1; + let backoff = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1); + if verbose { + crate::output::verbose(&format!( + "Server error {status}, retry {attempt}/{MAX_RETRIES} after {backoff}ms" + )); + } + tokio::time::sleep(Duration::from_millis(backoff)).await; + } + Ok(r) => return self.parse_futures_response(r, verbose).await, + Err(e) if is_transient(&e) && attempt < MAX_RETRIES => { + attempt += 1; + let backoff = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1); + if verbose { + crate::output::verbose(&format!( + "Transient error, retry {attempt}/{MAX_RETRIES} after {backoff}ms" + )); + } + tokio::time::sleep(Duration::from_millis(backoff)).await; + } + Err(e) => return Err(e.into()), + } + } + } + /// Execute a private GET on the Futures API with auth headers and retry /// on transient errors and 5xx server errors (GET is inherently idempotent). pub async fn private_get( diff --git a/src/commands/futures.rs b/src/commands/futures.rs index 04eb509..92336e6 100644 --- a/src/commands/futures.rs +++ b/src/commands/futures.rs @@ -75,6 +75,23 @@ pub(crate) enum FuturesCommand { /// Futures symbol (e.g. PI_XBTUSD). symbol: String, }, + /// Get OHLC candle data for a futures contract (no auth). + Ohlc { + /// Futures symbol (e.g. PF_XBTUSD). + symbol: String, + /// Candle resolution. + #[arg(long, default_value = "1d", value_parser = ["1m", "5m", "15m", "30m", "1h", "4h", "12h", "1d", "1w"])] + interval: String, + /// Tick type for the price series. + #[arg(long, default_value = "trade", value_parser = ["trade", "mark", "index"])] + tick_type: String, + /// Start of range, unix seconds (inclusive). + #[arg(long)] + from: Option, + /// End of range, unix seconds (inclusive). + #[arg(long)] + to: Option, + }, // === Private commands === /// Get futures account/wallet info (auth required). @@ -420,6 +437,30 @@ pub(crate) async fn execute( .await?; Ok(parse_generic(&data)) } + FuturesCommand::Ohlc { + symbol, + interval, + tick_type, + from, + to, + } => { + validate_path_segment(symbol, "symbol")?; + let mut params: Vec<(&str, &str)> = Vec::new(); + let from_owned; + if let Some(f) = from { + from_owned = f.to_string(); + params.push(("from", &from_owned)); + } + let to_owned; + if let Some(t) = to { + to_owned = t.to_string(); + params.push(("to", &to_owned)); + } + let data = client + .public_get_charts(tick_type, symbol, interval, ¶ms, verbose) + .await?; + Ok(parse_futures_ohlc(&data)) + } // === Private commands === FuturesCommand::Accounts => { @@ -1042,6 +1083,34 @@ fn parse_orderbook(data: &Value) -> CommandOutput { CommandOutput::new(data.clone(), headers, rows) } +/// Parse the futures charts OHLC response into Time | Open | High | Low | Close | Volume. +/// +/// NOTE: `time` is in millis since epoch (the OHLC endpoint uses seconds). +fn parse_futures_ohlc(data: &Value) -> CommandOutput { + let headers = vec![ + "Time".into(), + "Open".into(), + "High".into(), + "Low".into(), + "Close".into(), + "Volume".into(), + ]; + let mut rows = Vec::new(); + if let Some(candles) = data.get("candles").and_then(|c| c.as_array()) { + for candle in candles { + rows.push(vec![ + jstr(candle, "time"), + jstr(candle, "open"), + jstr(candle, "high"), + jstr(candle, "low"), + jstr(candle, "close"), + jstr(candle, "volume"), + ]); + } + } + CommandOutput::new(data.clone(), headers, rows) +} + /// Parse order status response (POST orders/status) into order_id | status table. fn parse_order_status(data: &Value) -> CommandOutput { let headers = vec!["order_id".into(), "status".into()]; @@ -1547,4 +1616,58 @@ mod tests { let params = build_history_params(None, None, None); assert!(params.is_empty()); } + + #[test] + fn parse_futures_ohlc_candle_fields() { + // curl -s -i 'https://futures.kraken.com/api/charts/v1/trade/PF_XBTUSD/1d?from=1735689600&to=1735862400' + let data = serde_json::json!({ + "candles": [ + { + "time": 1735689600000_u64, + "open": "93432", + "high": "95032", + "low": "92651", + "close": "94431", + "volume": "2267.6796" + }, + { + "time": 1735776000000_u64, + "open": "94431", + "high": "97774", + "low": "94210", + "close": "96918", + "volume": "3145.21" + } + ], + "more_candles": false + }); + + let cmd_out = parse_futures_ohlc(&data); + + assert_eq!(cmd_out.rows.len(), 2); + + assert_eq!( + cmd_out.rows[0], + vec![ + "1735689600000", + "93432", + "95032", + "92651", + "94431", + "2267.6796" + ] + ); + + assert_eq!( + cmd_out.rows[1], + vec![ + "1735776000000", + "94431", + "97774", + "94210", + "96918", + "3145.21" + ] + ); + } }