diff --git a/README.md b/README.md index f4466e4..64cb0a2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

Autonomous Trading Agent for Hyperliquid

- 14 strategies • APEX multi-slot orchestrator • REFLECT nightly review • MCP server • Agent Skills + 18 strategies • APEX multi-slot orchestrator • REFLECT nightly review • MCP server • Agent Skills

@@ -18,15 +18,15 @@

Python - Strategies - Tests + Strategies + Tests License - MCP + MCP

- - Deploy on Railway + + Launch via Nunchi Auth

@@ -135,7 +135,7 @@ Supporting strategies for portfolio management, block liquidity, and autonomous |----------|-------------|----------------|-------------| | `hedge_agent` | Inventory exposure reducer. Fires when net notional exceeds threshold. This is not the BTCSWP funding-rate hedge; use `hl hedge propose` / `hl hedge backtest` for that. | `notional_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. | | `rfq_agent` | Block-size dark RFQ liquidity — quotes for large orders with wider spreads. | `min_size`, `spread_bps` | Institutional/block flow. Provides hidden liquidity for large counterparties. | -| `claude_agent` | Multi-model LLM trading agent. Sends market snapshot to an LLM (Gemini, Claude, or OpenAI), receives structured trade decisions. | `model`, `base_size` | Experimental/research. Autonomous decision-making using LLM reasoning. | +| `ai_agent` | Nunchi-hosted LLM trading agent. Sends market snapshots to hosted inference and receives structured trade decisions. | `model`, `base_size` | Subscription-gated hosted-agent product; not required for BYO-agent MCP use. | ### Quoting Engine Pipeline @@ -146,10 +146,13 @@ Market Data -> Composite Fair Value -> Dynamic Spread -> Inventory Skew -> Multi (4-signal blend) (fee+vol+tox) (price+size) (exponential decay) ``` -### LLM Agent (Multi-Model) +### Hosted LLM Agent (Multi-Model) + +`ai_agent` is the hosted-agent product path. Generic MCP users bring their own agent and call the MCP tools directly; they do not need these provider keys. | Provider | Models | Env Variable | |----------|--------|-------------| +| OpenRouter | `openrouter/auto`, `openrouter/fusion` | `OPENROUTER_API_KEY` | | Google Gemini | `gemini-2.0-flash` (default), `gemini-2.5-pro` | `GEMINI_API_KEY` | | Anthropic Claude | `claude-haiku-4-5-20251001`, `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` | | OpenAI | `gpt-4o`, `gpt-4o-mini`, `o3-mini` | `OPENAI_API_KEY` | @@ -490,16 +493,48 @@ hl mcp serve # Start MCP server ## MCP Server -Expose all trading tools via [Model Context Protocol](https://modelcontextprotocol.io) for AI agent integration. +Expose all trading tools via [Model Context Protocol](https://modelcontextprotocol.io) for AI agent integration. This is the BYO-agent path: Cursor, Claude, Codex, or any custom MCP-capable agent calls MCP tools, and those tools invoke `agent-cli`. + +### Hosted MCP (Recommended) + +Use the hosted Nunchi MCP when you want a Robinhood-style setup: paste one URL into your AI client, authenticate with web-auth, select a wallet, and grant scoped tool access. Your AI client receives a scoped MCP token, not your exchange private key. + +```bash +https://agent.nunchi.trade/mcp/trading +``` + +Client setup: + +```bash +# Cursor +Settings -> Cursor Settings -> Tools & MCPs -> Connect +MCP URL: https://agent.nunchi.trade/mcp/trading + +# Claude Code +claude mcp add nunchi-trading --transport http https://agent.nunchi.trade/mcp/trading + +# Codex CLI +codex mcp add nunchi-trading --url https://agent.nunchi.trade/mcp/trading +``` + +After connecting, authenticate in the browser consent flow and start with: + +```text +Run setup_check, then show my account status and available strategies. +``` + +Hosted MCP access defaults to read-only/testnet. Write tools (`trade`, `run_strategy`, `apex_run`, `money_*`) and mainnet access require explicit web-auth consent and gateway-enforced limits. + +### Local MCP (Development Only) ```bash hl mcp serve # stdio transport (default) hl mcp serve --transport sse # SSE transport ``` -**18 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `funding_hedge_propose`, `funding_hedge_backtest`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `agent_memory`, `trade_journal`, `judge_report` +**25 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `funding_hedge_propose`, `funding_hedge_backtest`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `pair_status`, `approve_agent`, `money_withdraw`, `money_transfer_usd`, `money_deposit`, `money_bridge_status`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context` -Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. +Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. Local MCP is for development and agent harness testing only; hosted MCP goes through web-auth for wallet selection, consent, and scoped token minting. ### HTTP API & SSE @@ -509,59 +544,31 @@ Every deployed agent also exposes an HTTP REST API and SSE real-time feed for da --- -## Deploy on Railway - -Two deployment options: **headless** (APEX runs strategies directly) or **OpenClaw agent** (conversational AI trading assistant with Telegram). - -### Option A: Headless APEX (Deterministic) - -One-click deploy to run APEX autonomously. No AI model needed — pure deterministic strategy execution. - -[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/Nunchi-trade/agent-cli&envs=HL_PRIVATE_KEY,HL_TESTNET,RUN_MODE,APEX_PRESET&HL_TESTNETDefault=true&RUN_MODEDefault=apex&APEX_PRESETDefault=default) - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `HL_PRIVATE_KEY` | Yes | — | Your Hyperliquid private key | -| `HL_TESTNET` | No | `true` | `true` for testnet, `false` for mainnet | -| `RUN_MODE` | No | `apex` | `apex`, `wolf` (alias), `strategy`, or `mcp` | -| `APEX_PRESET` | No | `default` | `conservative`, `default`, or `aggressive` | +## Hosted Agent Deployment -**Run modes:** -- **apex** (default) — APEX multi-slot orchestrator with autonomous entry, exit, Guard trailing stops, and REFLECT self-improvement loop -- **strategy** — Single strategy loop (set `STRATEGY=engine_mm`, `avellaneda_mm`, etc.) -- **mcp** — MCP server for AI agent integration (SSE transport) +The only supported hosted deployment path is **Nunchi-hosted**: users pay in web-auth, bind an agent wallet, and Nunchi provisions and manages the agent on Nunchi-owned Railway infrastructure. -### Option B: OpenClaw Agent (Conversational AI) +Start here: -One-click deploy of a full OpenClaw agent that uses our CLI as the tool backend. Talk to your trading bot via Telegram — it scans markets, enters trades, manages risk, and learns from its mistakes. +[Launch a hosted agent through Nunchi Auth](https://auth.nunchi.trade) -[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/Nunchi-trade/agent-cli/tree/main/deploy/openclaw-railway&envs=HL_PRIVATE_KEY,AI_PROVIDER,AI_API_KEY,TELEGRAM_BOT_TOKEN,TELEGRAM_USERNAME,HL_TESTNET&HL_TESTNETDefault=true) +User flow: -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `HL_PRIVATE_KEY` | Yes | — | Your Hyperliquid private key | -| `AI_PROVIDER` | Yes | — | `anthropic`, `openai`, `gemini`, or `openrouter` | -| `AI_API_KEY` | Yes | — | API key for the chosen AI provider | -| `TELEGRAM_BOT_TOKEN` | Yes | — | Telegram bot token (from @BotFather) | -| `TELEGRAM_USERNAME` | Yes | — | Your Telegram @username | -| `HL_TESTNET` | No | `true` | `true` for testnet, `false` for mainnet | +1. Open web-auth and connect a wallet. +2. Bind or create an agent wallet. +3. Pay for the hosted-agent subscription with Stripe-supported payment methods or USDC. +4. Deploy the hosted agent from the wallet binding page. +5. Refresh status and open the hosted endpoint returned by web-auth. -**What you get:** -- OpenClaw gateway with web UI at `/openclaw` -- Telegram integration — chat with your bot to start/stop trading, run scans, check status -- Our 13 MCP trading tools as the agent's primary capabilities -- Persistent state across redeploys via `/data` volume -- Auto-onboard: bot sends "Agent ready" to Telegram on first deploy -- REFLECT self-improvement: the agent analyzes its own trades and adjusts strategy parameters +Hosted agents use Nunchi-owned inference credentials by default: -**How it works:** -1. Deploy sets up OpenClaw + our `hl mcp serve` as the tool provider -2. Bot auto-configures Telegram and sends you a ready message -3. Tell it "start trading" → it runs APEX with autonomous entry, exit, and risk management -4. Ask "how did we do?" → it runs REFLECT and reports performance metrics -5. The agent reads workspace files (AGENTS.md, SOUL.md) that define its trading behavior +| Variable | Default | Description | +|----------|---------|-------------| +| `AI_PROVIDER` | `openrouter` | OpenAI-compatible hosted inference | +| `AI_MODEL` | `openrouter/auto` | Low-latency/cost-aware default for chat and tool use | +| `NUNCHI_REFLECT_MODEL` | `openrouter/fusion` | Higher-confidence model for REFLECT/research-style analysis | -Both options persist state via Railway volume at `/data` — APEX state, REFLECT reports, Radar history, and agent memory survive redeploys. +Users do not need Railway credentials, Railway project access, or provider API keys. Billing, entitlement checks, wallet binding, secret injection, lifecycle controls, and hosted endpoint discovery all live in web-auth. This repository intentionally does not include Dockerfiles, `railway.toml`, or public deployment templates. --- @@ -588,12 +595,12 @@ hl run engine_mm -i BTCSWP-USDYP --tick 10 ``` cli/ CLI commands and trading engine commands/ Subcommand modules (run, apex, radar, pulse, guard, reflect, house, ...) - mcp_server.py MCP server (18 tools via FastMCP) + mcp_server.py MCP server (25 tools via FastMCP) hl_adapter.py Direct HL API adapter (live + mock) builder_fee.py Builder fee config (HL native BuilderInfo) keystore.py Encrypted keystore (geth-compatible) strategy_registry.py Strategy + YEX market definitions -strategies/ 14 trading strategy implementations +strategies/ 18 trading strategy implementations modules/ Pure logic modules (zero I/O) apex_engine.py APEX decision engine radar_engine.py Opportunity radar @@ -612,7 +619,7 @@ skills/ Agent Skills (SKILL.md + runners) sdk/ Strategy base class and model registry parent/ HL API proxy, position tracking, risk management scripts/ Backtest harness, bootstrap -tests/ Test suite (483 tests) +tests/ Test suite (1,317 tests) ``` --- @@ -664,9 +671,9 @@ hl run my_strategies.my_strategy:MyStrategy -i ETH-PERP --tick 10 | `HL_TESTNET` | No | `true` (default) or `false` for mainnet | | `BUILDER_ADDRESS` | No | Override builder fee address | | `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) | -| `ANTHROPIC_API_KEY` | No | For `claude_agent` with Claude | -| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | -| `OPENAI_API_KEY` | No | For `claude_agent` with OpenAI | +| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude | +| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini | +| `OPENAI_API_KEY` | No | For `ai_agent` with OpenAI | \* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required. @@ -676,7 +683,7 @@ hl run my_strategies.my_strategy:MyStrategy -i ETH-PERP --tick 10 ```bash pip install -e ".[dev]" -pytest tests/ -v # 483 tests +pytest tests/ -v # 1,317 tests ``` ## Attribution diff --git a/cli/commands/mcp.py b/cli/commands/mcp.py index 29acef9..5cccd11 100644 --- a/cli/commands/mcp.py +++ b/cli/commands/mcp.py @@ -26,5 +26,5 @@ def mcp_serve( raise typer.Exit(1) server = create_mcp_server() - typer.echo(f"Starting MCP server (transport={transport}) ...") + typer.echo(f"Starting MCP server (transport={transport}) ...", err=True) server.run(transport=transport) diff --git a/cli/commands/run.py b/cli/commands/run.py index dcbafdd..d627f26 100644 --- a/cli/commands/run.py +++ b/cli/commands/run.py @@ -53,7 +53,7 @@ def run_cmd( ), model: Optional[str] = typer.Option( None, "--model", - help="LLM model override for claude_agent strategy", + help="LLM model override for ai_agent strategy", ), ): """Start autonomous trading with a strategy.""" diff --git a/cli/commands/setup.py b/cli/commands/setup.py index 3a9717c..775b145 100644 --- a/cli/commands/setup.py +++ b/cli/commands/setup.py @@ -70,11 +70,17 @@ def setup_check(): else: ok_items.append("Builder fee: not configured (optional)") - # 5. LLM key (for claude_agent) - if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("GEMINI_API_KEY"): - ok_items.append("LLM API key found") + # 5. Hosted LLM key (only needed for the subscription-gated ai_agent product path) + if ( + os.environ.get("OPENROUTER_API_KEY") + or os.environ.get("AI_API_KEY") + or os.environ.get("ANTHROPIC_API_KEY") + or os.environ.get("GEMINI_API_KEY") + or os.environ.get("OPENAI_API_KEY") + ): + ok_items.append("Hosted LLM API key found") else: - ok_items.append("LLM API key: not set (only needed for claude_agent strategy)") + ok_items.append("Hosted LLM API key: not set (only needed for ai_agent)") # 6. Data directories data_dir = Path("data/cli") diff --git a/cli/mcp_server.py b/cli/mcp_server.py index 875b328..cf74b17 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -2,6 +2,11 @@ Fast tools (account, strategies, builder, wallet, setup) call Python directly. Long-running tools (run_strategy, apex_run, radar, reflect) use subprocess. + +Hosted MCP is the bring-your-own-agent path: a user's MCP-capable agent calls +these tools, while web-auth handles wallet selection, consent, and scoped access. +The OpenRouter-backed ai_agent strategy belongs to the separate hosted-agent +product and is not required for generic MCP use. """ from __future__ import annotations @@ -38,10 +43,12 @@ def create_mcp_server(): from mcp.server.fastmcp import FastMCP mcp = FastMCP( - "yex-trader", + "nunchi-trading", instructions=( - "Autonomous Hyperliquid trading CLI — 18 strategies, APEX orchestrator, " - "REFLECT reviews, and BTCSWP funding hedge proposals." + "Nunchi trading tools for MCP-capable agents. Bring your own agent; " + "web-auth selects the wallet and grants scoped tool access. Tools call " + "agent-cli for Hyperliquid/YEX trading, APEX orchestration, REFLECT " + "reviews, and BTCSWP funding hedge proposals." ), ) @@ -51,7 +58,12 @@ def create_mcp_server(): @mcp.tool() def strategies() -> str: - """List all available trading strategies with descriptions and default parameters.""" + """List available agent-cli strategies. + + Generic MCP clients can use any deterministic strategy directly. The + ai_agent strategy is the Nunchi-hosted LLM agent product path and may + require hosted-agent subscription/provider configuration. + """ from cli.strategy_registry import STRATEGY_REGISTRY, YEX_MARKETS result = {"strategies": {}, "yex_markets": {}} @@ -124,7 +136,7 @@ def wallet_auto(save_env: bool = True) -> str: @mcp.tool() def setup_check() -> str: - """Validate environment — SDK, keys, network, builder fee.""" + """Validate environment, web-auth pairing, network, and builder fee.""" import os from cli.keystore import list_keystores from cli.config import TradingConfig @@ -145,20 +157,21 @@ def setup_check() -> str: keystores = list_keystores() from cli.web_auth import get_stored_pairing pairing = get_stored_pairing() - if has_env_key: + if pairing is not None: + ok_items.append(f"Web-auth wallet selected ({pairing.selected_or_master_address})") + elif has_env_key: ok_items.append("HL_PRIVATE_KEY set") - if pairing is None: - warnings.append( - "Raw-key mode active. Prefer hl pair connect or hosted Nunchi Auth for MCP/agent use." - ) + warnings.append( + "Raw-key mode active. Prefer hl pair connect or hosted Nunchi Auth for MCP/agent use." + ) elif keystores: ok_items.append(f"Keystore found ({len(keystores)} keys)") else: issues.append("No private key: set HL_PRIVATE_KEY or run wallet_auto") - if pairing is not None: - ok_items.append(f"Paired wallet active ({pairing.selected_or_master_address})") - else: - warnings.append("No paired wallet found. Run hl pair connect to enable browser-approved signing.") + if pairing is None: + warnings.append( + "No web-auth wallet selected. Run hl pair connect, or complete the hosted MCP connect flow." + ) # Network testnet = os.environ.get("HL_TESTNET", "true").lower() @@ -176,12 +189,16 @@ def setup_check() -> str: "ok": ok_items, "warnings": warnings, "issues": issues, + "web_auth": { + "paired": pairing is not None, + "selected_wallet": pairing.selected_or_master_address if pairing is not None else None, + }, "passed": len(issues) == 0, }, indent=2) @mcp.tool() def pair_status() -> str: - """Show web-auth paired wallet status.""" + """Show the web-auth paired wallet selected for MCP tool calls.""" return _run_hl("pair", "status") @mcp.tool() diff --git a/cli/skill.md b/cli/skill.md index 0b7b574..b461069 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -94,8 +94,9 @@ hl apex run --mainnet # APEX multi-slot | `HL_TESTNET` | No | `true` (default) or `false` for mainnet | | `BUILDER_ADDRESS` | No | Override builder fee address (default: hardcoded) | | `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) | -| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy | -| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | +| `OPENROUTER_API_KEY` | No | For hosted `ai_agent` through OpenRouter | +| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude | +| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini | \* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required. @@ -205,7 +206,7 @@ Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_chec | aggressive_taker | Taker | Directional spread crossing with bias | | hedge_agent | Risk | Inventory exposure reducer; BTCSWP funding hedge lives under `hl hedge` | | rfq_agent | RFQ | Block-size dark RFQ liquidity | -| claude_agent | LLM | Claude/Gemini-powered autonomous trading agent | +| ai_agent | LLM | Hosted multi-provider autonomous trading agent | ## Instruments diff --git a/cli/strategy_registry.py b/cli/strategy_registry.py index 0ea02e3..6751c2f 100644 --- a/cli/strategy_registry.py +++ b/cli/strategy_registry.py @@ -34,9 +34,9 @@ "description": "Crosses the spread with directional bias", "params": {"size": 2.0, "bias_amplitude": 0.35}, }, - "claude_agent": { - "path": "strategies.claude_agent:ClaudeStrategy", - "description": "LLM trading agent — Gemini (default), Claude, OpenAI, or ClawRouter (x402 USDC)", + "ai_agent": { + "path": "strategies.ai_agent:AIStrategy", + "description": "Nunchi-hosted LLM trading agent — OpenRouter-backed by default, with configurable providers", "params": {"model": "gemini-2.0-flash", "base_size": 0.5}, }, "engine_mm": { diff --git a/docs/api-reference.md b/docs/api-reference.md index a746466..1de6cb5 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,14 +1,14 @@ # API Reference — Pulling Data from Nunchi Agents -This guide covers every method for pulling data from a running Nunchi agent. Three access paths depending on your deployment and use case: +This guide covers both Nunchi-hosted agents and the hosted trading MCP. Hosted agents are provisioned through web-auth after billing entitlement and wallet binding. Hosted MCP lets users bring their own MCP-capable agent, select a wallet in web-auth, and call agent-cli tools with scoped consent. | Path | Protocol | Best For | |------|----------|----------| | HTTP REST API | HTTP/JSON | Dashboards, monitoring, external integrations | | SSE Feed | Server-Sent Events | Live streaming to frontends | -| MCP Server | Model Context Protocol | AI agent orchestration (Claude, OpenClaw) | +| MCP Server | Model Context Protocol | BYO-agent trading tools through web-auth | -> **Security:** All endpoints are unauthenticated. If your agent is publicly accessible, anyone with the URL can read its state. Plan your network security accordingly. +> **Security:** Hosted MCP access is scoped by web-auth wallet selection and consent. The OpenRouter-backed `ai_agent` strategy is part of the subscription-gated hosted-agent product; generic MCP users bring their own agent and do not need Nunchi inference credentials. --- @@ -16,36 +16,21 @@ This guide covers every method for pulling data from a running Nunchi agent. Thr ### Agent Deployment -Your agent must be running in one of two deployment modes: +Deploy hosted agents from web-auth: -**Self-hosted (Python entrypoint):** - -```bash -# The entrypoint starts a health server on $PORT and the trading process -python scripts/entrypoint.py -``` - -The HTTP server binds to `0.0.0.0:$PORT` (default 8080). - -**Railway / OpenClaw (Node.js entrypoint):** - -```bash -# Express server with reverse proxy to OpenClaw gateway -node src/server.js +```text +https://auth.nunchi.trade ``` -The Express server binds to `0.0.0.0:$PORT` (default 8080) and exposes the same API surface. +The web-auth flow handles billing, wallet binding, provisioning through Nunchi's Railway account, secret injection, and hosted endpoint discovery. This repository does not publish public Docker/Railway deployment templates. ### Base URL Throughout this document, `$AGENT_URL` refers to your agent's base URL: ```bash -# Local development -export AGENT_URL=http://localhost:8080 - -# Railway deployment -export AGENT_URL=https://your-agent.up.railway.app +# Hosted endpoint returned by web-auth after deployment +export AGENT_URL=https://your-agent-hosted-endpoint.example # Verify connectivity curl $AGENT_URL/health @@ -84,8 +69,8 @@ curl $AGENT_URL/health | `alive` | bool | Whether the child process is still running | **Notes:** -- Used by Railway's health check system (`healthcheckPath` in `railway.toml`). -- Returns 200 even if the trading process has crashed — check `alive` to distinguish. +- Used by Nunchi's hosted-agent provisioner and health monitoring. +- Returns 200 if the HTTP server is responding; check `alive` to distinguish a live trading process from an unhealthy child process. --- @@ -216,7 +201,7 @@ curl $AGENT_URL/api/strategies "aggressive_taker": { "..." : "..." }, "hedge_agent": { "..." : "..." }, "rfq_agent": { "..." : "..." }, - "claude_agent": { "..." : "..." } + "ai_agent": { "..." : "..." } }, "markets": { "VXX-USDYP": "Volatility index yield perpetual", @@ -290,7 +275,8 @@ Use this as a connectivity + capability check before wiring a UI to the agent. Pauses the trading process by sending `SIGSTOP` to the child process. The agent stops executing ticks but maintains all state. Positions remain open. ```bash -curl -X POST $AGENT_URL/api/pause +curl -X POST $AGENT_URL/api/pause \ + -H "Authorization: Bearer $API_AUTH_TOKEN" ``` **Response:** @@ -305,6 +291,12 @@ curl -X POST $AGENT_URL/api/pause { "error": "No running agent to pause" } ``` +**Error (token not configured):** + +```json +{ "error": "control_auth_required" } +``` + > **Warning:** Pausing stops the DSL trailing stop from updating. If the market moves significantly while paused, positions will not be protected. --- @@ -314,7 +306,8 @@ curl -X POST $AGENT_URL/api/pause Resumes a paused trading process by sending `SIGCONT`. ```bash -curl -X POST $AGENT_URL/api/resume +curl -X POST $AGENT_URL/api/resume \ + -H "Authorization: Bearer $API_AUTH_TOKEN" ``` **Response:** @@ -332,7 +325,7 @@ All `/api/*` endpoints return CORS headers: ``` Access-Control-Allow-Origin: * (or $CORS_ORIGIN env var) Access-Control-Allow-Methods: GET, POST, OPTIONS -Access-Control-Allow-Headers: Content-Type, Authorization +Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Token ``` `OPTIONS` requests to any path return `204` with these headers. Set the `CORS_ORIGIN` environment variable to restrict origins in production. @@ -417,14 +410,7 @@ The leaderboard runs as a **separate microservice** from the agent. It tracks re ### Deployment -```bash -# From the cli-UI repo -cd deploy -docker build -t nunchi-leaderboard . -docker run -p 8090:8090 -v leaderboard-data:/data nunchi-leaderboard -``` - -Or deploy to Railway using the included `railway.toml`. +The leaderboard is operated as Nunchi infrastructure. This repository does not publish a public Docker or Railway deployment path for it. | Variable | Default | Description | |----------|---------|-------------| @@ -592,23 +578,23 @@ python leaderboard.py serve --port 8090 ## MCP Server -The MCP server exposes 16 tools for AI agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Claude Code, OpenClaw, or any MCP-compatible client. +The MCP server exposes 25 tools for BYO-agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Cursor, Claude Code, Codex, or any MCP-compatible custom agent. The agent calls MCP; MCP invokes `agent-cli`; web-auth handles wallet selection and scoped consent. ### Starting the Server ```bash -# stdio transport (for Claude Code / local AI agents) +# stdio transport (for local MCP-capable agents) hl mcp serve -# SSE transport (for remote connections) +# SSE transport (for development harnesses) hl mcp serve --transport sse ``` -Or set `RUN_MODE=mcp` in your Railway deployment. +Hosted users should use the web-auth hosted MCP connection instead of running their own remote MCP deployment. -### Connecting from Claude Code +### Connecting from an MCP Client -Add to your Claude Code MCP configuration: +Add to your MCP client configuration: ```json { @@ -627,7 +613,7 @@ These execute directly in Python with no subprocess overhead. #### `strategies()` -List all 14 trading strategies with descriptions and default parameters. +List all registered trading strategies with descriptions and default parameters. `ai_agent` is the hosted-agent product path; BYO MCP agents can call deterministic strategies and other tools directly. ``` Tool: strategies @@ -1031,8 +1017,8 @@ else: | `GET` | `/api/feed` | None | SSE stream | Persistent | | `GET` | `/status` | None | Plain text | <1s | | `POST` | `/api/skill/install` | None | JSON | <2s | -| `POST` | `/api/pause` | None | JSON | <10ms | -| `POST` | `/api/resume` | None | JSON | <10ms | +| `POST` | `/api/pause` | Auth header | JSON | <10ms | +| `POST` | `/api/resume` | Auth header | JSON | <10ms | ### Leaderboard Endpoints (separate service) diff --git a/docs/hl_feature_audit.md b/docs/hl_feature_audit.md index 44f3bab..bbadd4a 100644 --- a/docs/hl_feature_audit.md +++ b/docs/hl_feature_audit.md @@ -91,7 +91,7 @@ | **hedge_agent** | - | I | - | I | I | I | I | - | - | - | - | | **rfq_agent** | - | I | - | I | I | I | I | - | - | - | - | | **aggressive_taker** | - | I | - | I | I | I | I | - | - | - | - | -| **claude_agent** | - | I | - | I | I | I | I | - | - | - | - | +| **ai_agent** | - | I | - | I | I | I | I | - | - | - | - | | **engine_mm** | - | I | - | I | I | I | I | D | **D** | - | - | | **funding_arb** | - | I | - | I | I | I | I | D | **D** | - | - | | **regime_mm** | - | I | - | I | I | I | I | D | **D** | - | - | @@ -160,7 +160,7 @@ ### Strategy Groupings by HL Coupling **Loosely coupled (adapter-only dependency, 11 strategies):** -`simple_mm`, `avellaneda_mm`, `mean_reversion`, `hedge_agent`, `rfq_agent`, `aggressive_taker`, `claude_agent`, `momentum_breakout`, `grid_mm`, `simplified_ensemble`, `trend_follower` +`simple_mm`, `avellaneda_mm`, `mean_reversion`, `hedge_agent`, `rfq_agent`, `aggressive_taker`, `ai_agent`, `momentum_breakout`, `grid_mm`, `simplified_ensemble`, `trend_follower` These strategies have zero HL-specific imports. They only depend on HL through the adapter layer (`DirectHLProxy` / `MarketSnapshot`). Porting to another exchange requires only writing a new adapter that produces `MarketSnapshot` and accepts orders. diff --git a/requirements.txt b/requirements.txt index 13ea0e0..f73f0bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ eth-account>=0.10.0 requests>=2.28.0 eciespy>=0.4.0 -# Optional: LLM strategy (claude_agent) +# Optional: LLM strategy (ai_agent) # anthropic>=0.40.0 openai>=1.0.0 diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md index 557fb1c..208a558 100644 --- a/skills/onboard/SKILL.md +++ b/skills/onboard/SKILL.md @@ -179,6 +179,22 @@ hl builder status | `No private key` | Complete Step 2 | | `insufficient funds` | Complete Step 4 | +### Step 5b (institutional): designate a separate USDC settlement wallet + +Skip this step if you're trading on your own behalf. Required for HOUSE / Jump-style institutional pilots where Builder Code surcharges are remitted to a dedicated USDC wallet that is **not** the trading wallet. + +```bash +export SETTLEMENT_ADDRESS=0x +``` + +When `SETTLEMENT_ADDRESS` is set, the BC remittance ledger reports under that address. When it's empty (default), trading wallet doubles as settlement wallet. + +**Verify:** +```bash +hl builder status +# Expected line when set: "Settlement: 0x (separate from trading wallet)" +``` + --- ## Step 6: Validate with Mock Trade @@ -300,8 +316,11 @@ Only after completing Steps 1-8 on testnet: | `HL_TESTNET` | No | `true` (default) or `false` for mainnet | | `BUILDER_ADDRESS` | No | Override builder fee address | | `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) | -| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy | -| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | +| `SETTLEMENT_ADDRESS` | No | Institutional USDC settlement wallet (separate from trading wallet); empty = same as trading wallet | +| `MARKET_WHITELIST` | No | Comma-separated asset list or glob (e.g. `xyz:GOLD,xyz:CL` or `xyz:*`); empty = all HL perps | +| `OPENROUTER_API_KEY` | No | For hosted `ai_agent` through OpenRouter | +| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude | +| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini | \* Either keystore with `HL_KEYSTORE_PASSWORD` or `HL_PRIVATE_KEY` is required. diff --git a/strategies/claude_agent.py b/strategies/ai_agent.py similarity index 98% rename from strategies/claude_agent.py rename to strategies/ai_agent.py index 89c98f3..0c4b8a5 100644 --- a/strategies/claude_agent.py +++ b/strategies/ai_agent.py @@ -1,4 +1,8 @@ -"""LLM-powered trading agent — supports Claude, Gemini, OpenAI, OpenRouter, and ClawRouter. +"""Nunchi-hosted LLM trading agent. + +Supports OpenRouter, Claude, Gemini, OpenAI, and ClawRouter backends. This is +the hosted-agent product path. Generic MCP users bring their own external agent +and call the MCP tools directly. Uses structured tool/function calling to make trading decisions each tick. The LLM receives market data, position state, and risk context, then decides @@ -6,20 +10,20 @@ Usage: # Gemini (default — fast, free tier available) - hl run claude_agent --mock --max-ticks 5 --tick 15 - hl run claude_agent -i ETH-PERP --tick 15 + hl run ai_agent --mock --max-ticks 5 --tick 15 + hl run ai_agent -i ETH-PERP --tick 15 # Claude - hl run claude_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001 + hl run ai_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001 # Gemini Flash - hl run claude_agent -i ETH-PERP --tick 15 --model gemini-2.0-flash + hl run ai_agent -i ETH-PERP --tick 15 --model gemini-2.0-flash # ClawRouter (x402 — pay with USDC, no API key needed) - hl run claude_agent -i ETH-PERP --tick 15 --model blockrun/auto + hl run ai_agent -i ETH-PERP --tick 15 --model blockrun/auto # OpenRouter (Nunchi hosted default) - hl run claude_agent -i ETH-PERP --tick 15 --model openrouter/auto + hl run ai_agent -i ETH-PERP --tick 15 --model openrouter/auto """ from __future__ import annotations @@ -136,12 +140,12 @@ def _env_bool(name: str, default: bool = False) -> bool: # --------------------------------------------------------------------------- -class ClaudeStrategy(BaseStrategy): +class AIStrategy(BaseStrategy): """LLM-powered trading strategy with multiple hosted/local inference backends.""" def __init__( self, - strategy_id: str = "claude_agent", + strategy_id: str = "ai_agent", model: str = "gemini-2.0-flash", base_size: float = 0.5, max_position: float = 5.0, diff --git a/tests/e2e/test_bounded_daemons.py b/tests/e2e/test_bounded_daemons.py index f2738b8..7dd830f 100644 --- a/tests/e2e/test_bounded_daemons.py +++ b/tests/e2e/test_bounded_daemons.py @@ -82,7 +82,8 @@ def test_mcp_serve_fails_cleanly_without_optional_extra_or_can_show_startup(run_ assert "Starting MCP server" in result.combined_output return if result.returncode == 0: - pytest.fail("mcp serve unexpectedly exited successfully; it should either run or report missing extras") + assert "Starting MCP server" in result.combined_output + return assert ( "MCP package not installed" in result.combined_output or "Starting MCP server" in result.combined_output diff --git a/tests/e2e/test_entrypoint_http.py b/tests/e2e/test_entrypoint_http.py index 1cda0bf..b76f1e3 100644 --- a/tests/e2e/test_entrypoint_http.py +++ b/tests/e2e/test_entrypoint_http.py @@ -18,7 +18,7 @@ @pytest.fixture def entrypoint_server(monkeypatch, tmp_path): monkeypatch.setenv("RUN_MODE", "strategy") - monkeypatch.setenv("STRATEGY", "claude_agent") + monkeypatch.setenv("STRATEGY", "ai_agent") monkeypatch.setenv("AI_PROVIDER", "openrouter") monkeypatch.setenv("AI_MODEL", "openrouter/fusion") monkeypatch.setenv("HL_TESTNET", "true") @@ -66,7 +66,7 @@ def test_health_status_metrics_and_pricing_endpoints(entrypoint_server): assert status == 200 assert health["status"] == "ok" assert health["mode"] == "strategy" - assert health["strategy"] == "claude_agent" + assert health["strategy"] == "ai_agent" status, api_status = _request_json(base_url, "/api/status") assert status == 200 diff --git a/tests/e2e/test_strategy_smoke.py b/tests/e2e/test_strategy_smoke.py index 8d478b9..7c90fe2 100644 --- a/tests/e2e/test_strategy_smoke.py +++ b/tests/e2e/test_strategy_smoke.py @@ -10,7 +10,7 @@ pytestmark = [pytest.mark.e2e, pytest.mark.slow] -LLM_OR_LIVE_ONLY = {"claude_agent"} +LLM_OR_LIVE_ONLY = {"ai_agent"} @pytest.mark.parametrize("strategy_name", sorted(STRATEGY_REGISTRY)) @@ -71,17 +71,17 @@ def test_registered_strategy_runs_deeper_mock_loop_and_status(run_cli, tmp_path, @pytest.mark.live @pytest.mark.llm -def test_claude_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path): +def test_ai_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path): import os if not (os.environ.get("OPENROUTER_API_KEY") or os.environ.get("AI_API_KEY")): pytest.skip("OPENROUTER_API_KEY or AI_API_KEY is required for OpenRouter E2E") - data_dir = tmp_path / "claude-agent-openrouter" + data_dir = tmp_path / "ai-agent-openrouter" result = run_cli( [ "run", - "claude_agent", + "ai_agent", "--mock", "--max-ticks", "1", @@ -101,7 +101,7 @@ def test_claude_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path): ) assert "Mode: MOCK" in result.stdout - assert "Strategy: claude_agent" in result.stdout + assert "Strategy: ai_agent" in result.stdout assert (data_dir / "state.db").exists() diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index f12cd12..06fad0d 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -12,6 +12,7 @@ build_command, MAX_BODY_SIZE, _SECRET_RE, + _pricing_snapshot, HealthHandler, ) @@ -78,6 +79,8 @@ def test_strategy_mode(self, monkeypatch): monkeypatch.setenv("INSTRUMENT", "BTC-PERP") monkeypatch.setenv("TICK_INTERVAL", "5") monkeypatch.setenv("HL_TESTNET", "true") + monkeypatch.delenv("AI_MODEL", raising=False) + monkeypatch.delenv("MAX_TICKS", raising=False) cmd = build_command() assert "run" in cmd @@ -86,6 +89,30 @@ def test_strategy_mode(self, monkeypatch): assert "BTC-PERP" in cmd assert "-t" in cmd assert "5" in cmd + assert "--data-dir" in cmd + assert "/data/cli" in cmd + assert "--mainnet" not in cmd + + def test_strategy_mode_hosted_pricing_options(self, monkeypatch): + monkeypatch.setenv("RUN_MODE", "strategy") + monkeypatch.setenv("STRATEGY", "ai_agent") + monkeypatch.setenv("INSTRUMENT", "ETH-PERP") + monkeypatch.setenv("TICK_INTERVAL", "10") + monkeypatch.setenv("DATA_DIR", "/data/pricing") + monkeypatch.setenv("AI_MODEL", "openrouter/fusion") + monkeypatch.setenv("MAX_TICKS", "100") + monkeypatch.setenv("HL_TESTNET", "true") + + cmd = build_command() + + assert cmd[:3] == [sys.executable, "-m", "cli.main"] + assert cmd[3:5] == ["run", "ai_agent"] + assert "--data-dir" in cmd + assert "/data/pricing" in cmd + assert "--model" in cmd + assert "openrouter/fusion" in cmd + assert "--max-ticks" in cmd + assert "100" in cmd assert "--mainnet" not in cmd def test_strategy_mode_mainnet(self, monkeypatch): @@ -108,6 +135,34 @@ def test_unknown_mode_exits(self, monkeypatch): build_command() +class TestPricingSnapshot: + def test_pricing_snapshot_reports_env_and_ledgers(self, monkeypatch, tmp_path): + monkeypatch.setenv("RUN_MODE", "strategy") + monkeypatch.setenv("STRATEGY", "ai_agent") + monkeypatch.setenv("AI_PROVIDER", "openrouter") + monkeypatch.setenv("AI_MODEL", "openrouter/fusion") + monkeypatch.setenv("HL_TESTNET", "true") + monkeypatch.setenv("NUNCHI_EXPERIMENT_ID", "exp-1") + monkeypatch.setenv("NUNCHI_RUN_ID", "run-1") + monkeypatch.setenv("NUNCHI_JOB_TYPE", "taker") + monkeypatch.setenv("NUNCHI_AGENT_ID", "taker-01") + (tmp_path / "cost_ledger.jsonl").write_text('{"usd_cost":"0.001"}\n') + (tmp_path / "route_ledger.jsonl").write_text('{"requested_route":"openrouter/fusion"}\n') + + snapshot = _pricing_snapshot(str(tmp_path), limit=10) + + assert snapshot["mode"] == "strategy" + assert snapshot["strategy"] == "ai_agent" + assert snapshot["ai_provider"] == "openrouter" + assert snapshot["ai_model"] == "openrouter/fusion" + assert snapshot["experiment_id"] == "exp-1" + assert snapshot["job_type"] == "taker" + assert snapshot["ledger_exists"]["cost"] is True + assert snapshot["ledger_exists"]["route"] is True + assert snapshot["ledgers"]["cost"][0]["usd_cost"] == "0.001" + assert snapshot["ledgers"]["route"][0]["requested_route"] == "openrouter/fusion" + + # --------------------------------------------------------------------------- # MAX_BODY_SIZE # --------------------------------------------------------------------------- @@ -166,7 +221,9 @@ def test_no_token_configured(self, monkeypatch): ep.AUTH_TOKEN = None try: handler = self._make_handler() - assert handler._check_auth() is True + assert handler._check_auth() is False + handler.send_response.assert_called_with(503) + handler.write.assert_called_once() finally: ep.AUTH_TOKEN = original @@ -180,6 +237,17 @@ def test_valid_token(self, monkeypatch): finally: ep.AUTH_TOKEN = original + def test_valid_x_api_token(self, monkeypatch): + import scripts.entrypoint as ep + original = ep.AUTH_TOKEN + ep.AUTH_TOKEN = "test-secret" + try: + handler = self._make_handler() + handler.headers = {"Authorization": "", "X-API-Token": "test-secret"} + assert handler._check_auth() is True + finally: + ep.AUTH_TOKEN = original + def test_invalid_token(self, monkeypatch): import scripts.entrypoint as ep original = ep.AUTH_TOKEN diff --git a/tests/test_mcp_money_tools.py b/tests/test_mcp_money_tools.py index 9cb67c7..a729736 100644 --- a/tests/test_mcp_money_tools.py +++ b/tests/test_mcp_money_tools.py @@ -1,8 +1,10 @@ """Smoke tests for MCP money-movement wrappers.""" from __future__ import annotations +import json import sys import types +from types import SimpleNamespace class FakeFastMCP: @@ -20,6 +22,127 @@ def decorator(fn): return decorator +def install_fake_mcp(monkeypatch) -> None: + fastmcp_module = types.ModuleType("mcp.server.fastmcp") + fastmcp_module.FastMCP = FakeFastMCP + server_module = types.ModuleType("mcp.server") + server_module.fastmcp = fastmcp_module + mcp_module = types.ModuleType("mcp") + mcp_module.server = server_module + monkeypatch.setitem(sys.modules, "mcp", mcp_module) + monkeypatch.setitem(sys.modules, "mcp.server", server_module) + monkeypatch.setitem(sys.modules, "mcp.server.fastmcp", fastmcp_module) + + +def test_mcp_strategies_exposes_ai_agent_without_legacy_name(monkeypatch): + install_fake_mcp(monkeypatch) + + from cli.mcp_server import create_mcp_server + + server = create_mcp_server() + payload = json.loads(server.tools["strategies"]()) + + assert "ai_agent" in payload["strategies"] + assert "Nunchi-hosted LLM trading agent" in payload["strategies"]["ai_agent"]["description"] + assert "claude_agent" not in payload["strategies"] + + +def test_mcp_setup_check_treats_web_auth_pairing_as_wallet_selection(monkeypatch): + install_fake_mcp(monkeypatch) + monkeypatch.delenv("HL_PRIVATE_KEY", raising=False) + + import cli.keystore as keystore + import cli.web_auth as web_auth + from cli.mcp_server import create_mcp_server + + monkeypatch.setattr(keystore, "list_keystores", lambda: []) + monkeypatch.setattr( + web_auth, + "get_stored_pairing", + lambda: SimpleNamespace(selected_or_master_address="0x1111111111111111111111111111111111111111"), + ) + + server = create_mcp_server() + payload = json.loads(server.tools["setup_check"]()) + + assert payload["web_auth"] == { + "paired": True, + "selected_wallet": "0x1111111111111111111111111111111111111111", + } + assert payload["passed"] is True + assert not any("No private key" in issue for issue in payload["issues"]) + + +def test_mcp_pair_status_uses_agent_cli_pair_status(monkeypatch): + install_fake_mcp(monkeypatch) + + import cli.mcp_server as mcp_server + + calls = [] + monkeypatch.setattr(mcp_server, "_run_hl", lambda *args, timeout=30: calls.append(args) or "Pairing: NONE") + + server = mcp_server.create_mcp_server() + + assert server.tools["pair_status"]() == "Pairing: NONE" + assert calls == [("pair", "status")] + + +def test_mcp_funding_hedge_propose_is_read_only_json(monkeypatch): + install_fake_mcp(monkeypatch) + + from cli.mcp_server import create_mcp_server + + server = create_mcp_server() + payload = json.loads(server.tools["funding_hedge_propose"]( + asset="BTC", + perp_side="long", + perp_notional_usd=150_000.0, + funding_apr=0.45, + )) + + assert payload["asset"] == "BTC" + assert payload["perp_side"] == "long" + assert payload["perp_notional_usd"] == 150_000.0 + assert payload["hedge_notional_usd"] > 0 + + +def test_mcp_run_strategy_builds_bounded_mock_agent_cli_call(monkeypatch): + install_fake_mcp(monkeypatch) + + import cli.mcp_server as mcp_server + + calls = [] + monkeypatch.setattr(mcp_server, "_run_hl", lambda *args, timeout=30: calls.append((args, timeout)) or "ok") + + server = mcp_server.create_mcp_server() + + assert server.tools["run_strategy"]( + "engine_mm", + instrument="ETH-PERP", + tick=0, + max_ticks=1, + mock=True, + dry_run=True, + ) == "ok" + assert calls == [ + ( + ( + "run", + "engine_mm", + "-i", + "ETH-PERP", + "-t", + "0", + "--max-ticks", + "1", + "--mock", + "--dry-run", + ), + 60, + ) + ] + + def test_mcp_money_tools_require_confirm(monkeypatch): fastmcp_module = types.ModuleType("mcp.server.fastmcp") fastmcp_module.FastMCP = FakeFastMCP diff --git a/tests/test_strategy_claude_agent.py b/tests/test_strategy_ai_agent.py similarity index 84% rename from tests/test_strategy_claude_agent.py rename to tests/test_strategy_ai_agent.py index 4d7e5e7..6fc4010 100644 --- a/tests/test_strategy_claude_agent.py +++ b/tests/test_strategy_ai_agent.py @@ -1,4 +1,4 @@ -"""Tests for ClaudeStrategy (LLM agent) — tests helper functions only, no API calls.""" +"""Tests for AIStrategy (LLM agent) — tests helper functions only, no API calls.""" import os import sys import time @@ -32,37 +32,37 @@ def _ctx(pos_qty=0.0, upnl=0.0, rpnl=0.0, reduce_only=False, safe_mode=False, ro class TestDetectProvider: def test_claude_model(self): - from strategies.claude_agent import _detect_provider + from strategies.ai_agent import _detect_provider assert _detect_provider("claude-haiku-4-5-20251001") == "claude" assert _detect_provider("claude-3-sonnet") == "claude" def test_gemini_model(self): - from strategies.claude_agent import _detect_provider + from strategies.ai_agent import _detect_provider assert _detect_provider("gemini-2.0-flash") == "gemini" assert _detect_provider("gemini-pro") == "gemini" def test_openai_model(self): - from strategies.claude_agent import _detect_provider + from strategies.ai_agent import _detect_provider assert _detect_provider("gpt-4o") == "openai" assert _detect_provider("o1-mini") == "openai" assert _detect_provider("o3-mini") == "openai" assert _detect_provider("o4-mini") == "openai" def test_blockrun_model(self): - from strategies.claude_agent import _detect_provider + from strategies.ai_agent import _detect_provider assert _detect_provider("blockrun/auto") == "blockrun" assert _detect_provider("blockrun/claude-sonnet") == "blockrun" def test_unknown_defaults_to_gemini(self): - from strategies.claude_agent import _detect_provider + from strategies.ai_agent import _detect_provider assert _detect_provider("some-random-model") == "gemini" assert _detect_provider("") == "gemini" class TestParseToolCall: def _make_strat(self): - from strategies.claude_agent import ClaudeStrategy - return ClaudeStrategy(base_size=0.5, max_position=5.0) + from strategies.ai_agent import AIStrategy + return AIStrategy(base_size=0.5, max_position=5.0) def test_valid_place_order(self): strat = self._make_strat() @@ -166,8 +166,8 @@ def test_instrument_from_snapshot(self): class TestBuildUserMessage: def _make_strat(self): - from strategies.claude_agent import ClaudeStrategy - return ClaudeStrategy(base_size=0.5, max_position=5.0) + from strategies.ai_agent import AIStrategy + return AIStrategy(base_size=0.5, max_position=5.0) def test_contains_market_data(self): strat = self._make_strat() @@ -218,8 +218,8 @@ def test_no_context_uses_question_mark(self): class TestBuildOpenAITools: def test_format(self): - from strategies.claude_agent import ClaudeStrategy - strat = ClaudeStrategy() + from strategies.ai_agent import AIStrategy + strat = AIStrategy() tools = strat._build_openai_tools() assert len(tools) == 2 for t in tools: @@ -229,8 +229,8 @@ def test_format(self): assert "parameters" in t["function"] def test_place_order_tool(self): - from strategies.claude_agent import ClaudeStrategy - strat = ClaudeStrategy() + from strategies.ai_agent import AIStrategy + strat = AIStrategy() tools = strat._build_openai_tools() place_order = [t for t in tools if t["function"]["name"] == "place_order"][0] params = place_order["function"]["parameters"] @@ -239,8 +239,8 @@ def test_place_order_tool(self): assert "price" in params["properties"] def test_hold_tool(self): - from strategies.claude_agent import ClaudeStrategy - strat = ClaudeStrategy() + from strategies.ai_agent import AIStrategy + strat = AIStrategy() tools = strat._build_openai_tools() hold = [t for t in tools if t["function"]["name"] == "hold"][0] params = hold["function"]["parameters"] @@ -249,34 +249,34 @@ def test_hold_tool(self): class TestOpenRouterFusion: def test_fusion_defaults_to_requested_route(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.delenv("OPENROUTER_FUSION_MODEL", raising=False) monkeypatch.delenv("NUNCHI_OPENROUTER_FUSION_MODEL", raising=False) - strat = ClaudeStrategy(model="openrouter/fusion") + strat = AIStrategy(model="openrouter/fusion") assert strat._resolve_openrouter_model() == "openrouter/fusion" def test_fusion_can_be_overridden(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.setenv("OPENROUTER_FUSION_MODEL", "anthropic/claude-haiku") - strat = ClaudeStrategy(model="openrouter/fusion") + strat = AIStrategy(model="openrouter/fusion") assert strat._resolve_openrouter_model() == "anthropic/claude-haiku" def test_fusion_plugin_uses_budget_preset(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.setenv("OPENROUTER_FUSION_PRESET", "general-budget") - strat = ClaudeStrategy(model="openrouter/fusion") + strat = AIStrategy(model="openrouter/fusion") assert strat._openrouter_fusion_plugins() == [ {"id": "fusion", "preset": "general-budget"} ] def test_fusion_plugin_supports_explicit_panel(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.setenv( "OPENROUTER_FUSION_ANALYSIS_MODELS", @@ -284,7 +284,7 @@ def test_fusion_plugin_supports_explicit_panel(self, monkeypatch): ) monkeypatch.setenv("OPENROUTER_FUSION_JUDGE_MODEL", "openai/gpt-5-nano") monkeypatch.setenv("OPENROUTER_FUSION_MAX_TOOL_CALLS", "1") - strat = ClaudeStrategy(model="openrouter/fusion") + strat = AIStrategy(model="openrouter/fusion") assert strat._openrouter_fusion_plugins() == [ { @@ -299,19 +299,19 @@ def test_fusion_plugin_supports_explicit_panel(self, monkeypatch): ] def test_force_fusion_env(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.setenv("OPENROUTER_FORCE_FUSION", "true") - strat = ClaudeStrategy(model="openrouter/fusion") + strat = AIStrategy(model="openrouter/fusion") assert strat._force_openrouter_fusion() is True def test_llm_decision_interval_skips_intermediate_ticks(self, monkeypatch): - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy monkeypatch.setenv("NUNCHI_LLM_DECISION_INTERVAL_TICKS", "3") calls = [] - strat = ClaudeStrategy(model="gemini-2.0-flash") + strat = AIStrategy(model="gemini-2.0-flash") def fake_call(user_msg, snapshot): calls.append(strat._current_tick_index) @@ -325,24 +325,24 @@ def fake_call(user_msg, snapshot): assert calls == [1, 4] -class TestClaudeStrategyOnTick: +class TestAIStrategyOnTick: def test_zero_mid_returns_empty(self): - from strategies.claude_agent import ClaudeStrategy - strat = ClaudeStrategy() + from strategies.ai_agent import AIStrategy + strat = AIStrategy() orders = strat.on_tick(_snap(mid=0.0, bid=0.0, ask=0.0), _ctx()) assert orders == [] def test_safe_mode_returns_empty(self): - from strategies.claude_agent import ClaudeStrategy - strat = ClaudeStrategy() + from strategies.ai_agent import AIStrategy + strat = AIStrategy() orders = strat.on_tick(_snap(), _ctx(safe_mode=True)) assert orders == [] def test_on_tick_without_api_key_returns_empty(self): """Without API keys, on_tick should catch the error and return [].""" - from strategies.claude_agent import ClaudeStrategy + from strategies.ai_agent import AIStrategy # Use gemini model (default) — no API key set - strat = ClaudeStrategy(model="gemini-2.0-flash") + strat = AIStrategy(model="gemini-2.0-flash") # Clear any env vars old_key = os.environ.pop("GEMINI_API_KEY", None) old_key2 = os.environ.pop("GOOGLE_API_KEY", None) diff --git a/tests/test_strategy_registry.py b/tests/test_strategy_registry.py index a72afd8..5e3d74e 100644 --- a/tests/test_strategy_registry.py +++ b/tests/test_strategy_registry.py @@ -31,9 +31,9 @@ def test_error_shows_available(self): with pytest.raises(ValueError, match="simple_mm"): resolve_strategy_path("bad_name") - def test_claude_agent_registered(self): - path = resolve_strategy_path("claude_agent") - assert "ClaudeStrategy" in path + def test_ai_agent_registered(self): + path = resolve_strategy_path("ai_agent") + assert "AIStrategy" in path def test_registry_has_params(self): for name, entry in STRATEGY_REGISTRY.items():