Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.

Expand Down Expand Up @@ -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 |
|-------|----------|------|-------------|
Expand Down Expand Up @@ -481,6 +481,7 @@ kraken balance -o json -v 2>/dev/null | jq .
| `kraken futures ticker <SYMBOL>` | Single ticker |
| `kraken futures orderbook <SYMBOL>` | Order book |
| `kraken futures history <SYMBOL> [--since TS] [--before TS]` | Trade history |
| `kraken futures ohlc <SYMBOL> [--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 |

Expand Down
42 changes: 42 additions & 0 deletions agents/tool-catalog.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value> {
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(
Expand Down
123 changes: 123 additions & 0 deletions src/commands/futures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
/// End of range, unix seconds (inclusive).
#[arg(long)]
to: Option<u64>,
},

// === Private commands ===
/// Get futures account/wallet info (auth required).
Expand Down Expand Up @@ -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, &params, verbose)
.await?;
Ok(parse_futures_ohlc(&data))
}

// === Private commands ===
FuturesCommand::Accounts => {
Expand Down Expand Up @@ -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()];
Expand Down Expand Up @@ -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"
]
);
}
}