diff --git a/README.md b/README.md index f69ebcc..d179b70 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Market Data -> Composite Fair Value -> Dynamic Spread -> Inventory Skew -> Multi | Provider | Models | Env Variable | |----------|--------|-------------| | 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` | +| Anthropic Claude | `claude-haiku-4-5-20251001`, `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` or `ANTHROPIC_SESSION_TOKEN` | | OpenAI | `gpt-4o`, `gpt-4o-mini`, `o3-mini` | `OPENAI_API_KEY` | --- @@ -648,7 +648,8 @@ 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 | +| `ANTHROPIC_API_KEY` | No | For `claude_agent` with Claude (API key) | +| `ANTHROPIC_SESSION_TOKEN` | No | For `claude_agent` with Claude (Claude Max session token) | | `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | | `OPENAI_API_KEY` | No | For `claude_agent` with OpenAI | diff --git a/cli/commands/mcp.py b/cli/commands/mcp.py index 29acef9..7dc7726 100644 --- a/cli/commands/mcp.py +++ b/cli/commands/mcp.py @@ -13,6 +13,8 @@ def mcp_serve( transport: str = typer.Option("stdio", "--transport", "-t", help="Transport mode: stdio or sse"), + port: int = typer.Option(18790, "--port", "-p", + help="Port for SSE transport (ignored for stdio)"), ): """Start MCP server exposing trading tools for AI agents.""" project_root = str(Path(__file__).resolve().parent.parent.parent) diff --git a/cli/commands/setup.py b/cli/commands/setup.py index 5c10152..b437f52 100644 --- a/cli/commands/setup.py +++ b/cli/commands/setup.py @@ -59,10 +59,10 @@ def setup_check(): 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") + if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_SESSION_TOKEN") or os.environ.get("GEMINI_API_KEY"): + ok_items.append("LLM API key/session token found") else: - ok_items.append("LLM API key: not set (only needed for claude_agent strategy)") + ok_items.append("LLM API key/session token: not set (only needed for claude_agent strategy)") # 6. Data directories data_dir = Path("data/cli") diff --git a/cli/hl_adapter.py b/cli/hl_adapter.py index f8b898d..9fc02a1 100644 --- a/cli/hl_adapter.py +++ b/cli/hl_adapter.py @@ -168,8 +168,8 @@ def get_account_state(self) -> Dict: log.error("Failed to get account state: %s", e) return {} - # Merge HIP-3 DEX positions (e.g. YEX) so watchdog/reconciliation sees them. - for dex_id in HIP3_DEXS: + # Merge HIP-3 DEX positions (e.g. YEX) — testnet only, not available on mainnet. + for dex_id in (HIP3_DEXS if self._hl.testnet else {}): try: dex_state = self._info.post("/info", { "type": "clearinghouseState", "user": self._address, "dex": dex_id, @@ -324,18 +324,25 @@ def place_order( # Round price to HL tick size (price-dependent, 5 sig figs) price = self._round_price(price, coin) - # For IOC orders, apply slippage to cross the spread and guarantee fill. - # Strategy prices are often at fair value (inside the spread) which won't - # match any resting orders. Push buys above ask, sells below bid. - if tif == "Ioc": - try: - snap = self._hl.get_snapshot(instrument) + # Price adjustment based on order type: + # - IOC: push past spread to guarantee fill (taker) + # - ALO: sit on maker side of book for rebates + try: + snap = self._hl.get_snapshot(instrument) + if tif == "Ioc": if is_buy and snap.ask > 0: - price = max(price, self._round_price(snap.ask * SLIPPAGE_FACTOR, coin)) + price = max(price, self._round_price( + snap.ask * SLIPPAGE_FACTOR, coin)) elif not is_buy and snap.bid > 0: - price = min(price, self._round_price(snap.bid * (2 - SLIPPAGE_FACTOR), coin)) - except Exception: - pass # use original price if snapshot fails + price = min(price, self._round_price( + snap.bid * (2 - SLIPPAGE_FACTOR), coin)) + elif tif == "Alo": + if is_buy and snap.bid > 0: + price = self._round_price(snap.bid, coin) + elif not is_buy and snap.ask > 0: + price = self._round_price(snap.ask, coin) + except Exception: + pass # use original price if snapshot fails fill = self._send_order(coin, instrument, side, is_buy, size, price, tif, builder) @@ -508,6 +515,7 @@ def place_trigger_order(self, instrument: str, side: str, size: float, trigger_p coin = self._to_coin(instrument) is_buy = side.lower() == "buy" sz = self._round_size(coin, size) + trigger_price = self._round_price(trigger_price, coin) try: result = self._exchange.order( coin, is_buy, sz, trigger_price, diff --git a/cli/skill.md b/cli/skill.md index 9cd0388..f07364b 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -94,7 +94,8 @@ 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 | +| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy (API key) | +| `ANTHROPIC_SESSION_TOKEN` | No | For `claude_agent` strategy (Claude Max session token) | | `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | \* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required. diff --git a/configs/apex_btc_mainnet.yaml b/configs/apex_btc_mainnet.yaml new file mode 100644 index 0000000..40c937c --- /dev/null +++ b/configs/apex_btc_mainnet.yaml @@ -0,0 +1,17 @@ +# APEX config — BTC-PERP only, mainnet, $50 account +# Single parameter focus: radar_score_threshold + +# Instrument filter +allowed_instruments: + - BTC-PERP + +# Entry threshold — the single parameter to tune +radar_score_threshold: 180 + +# Scaled for $50 account +total_budget: 50.0 +max_slots: 1 +leverage: 10.0 +daily_loss_limit: 10.0 +guard_preset: tight +slot_cooldown_ms: 7200000 # 2 hours — prevents fee churn on re-entry diff --git a/configs/autoresearch_program.md b/configs/autoresearch_program.md index 0866536..d689402 100644 --- a/configs/autoresearch_program.md +++ b/configs/autoresearch_program.md @@ -1,7 +1,8 @@ # Autoresearch Program: APEX Config Optimization ## Objective -Optimize APEX trading strategy parameters by replaying historical trades through the backtest harness and maximizing net PnL while maintaining trade quality. +Optimize the single entry threshold parameter for BTC-PERP mainnet trading by replaying +historical trades through the backtest harness and maximizing net PnL. ## Mutable File `apex_config.json` @@ -20,42 +21,33 @@ python3 scripts/backtest_apex.py --config apex_config.json --trades data/cli/tra - `trades` — should stay above 5 (quality gate) - `profit_factor` — should stay above 1.0 -## Parameter Bounds (guardrails) +## Single Parameter: radar_score_threshold -| Parameter | Min | Max | Step | Default | -|------------------------------|-------|-------|------|---------| -| radar_score_threshold | 120 | 280 | 10 | 170 | -| pulse_confidence_threshold | 40.0 | 95.0 | 5.0 | 70.0 | -| daily_loss_limit | 50.0 | 5000.0| 50.0 | 500.0 | -| max_same_direction | 1 | 3 | 1 | 2 | +| Parameter | Min | Max | Step | Default | +|-----------------------|-----|-----|------|---------| +| radar_score_threshold | 120 | 280 | 10 | 170 | -## Research Directions - -These are common exploration paths based on REFLECT findings: - -1. **High FDR (>30%)**: Raise `radar_score_threshold` in [170, 250] to filter low-quality entries that generate fees without sufficient edge. - -2. **Low Win Rate (<40%)**: Sweep `pulse_confidence_threshold` in [70, 95] to require higher conviction before entry. +All other parameters are fixed. Do not modify them. -3. **Direction Imbalance**: If one direction is consistently losing, set `max_same_direction` to 1 to force diversification. - -4. **Loss Streaks**: Reduce `daily_loss_limit` by 20% increments to cut drawdowns from consecutive losses. - -5. **Healthy Strategy**: If metrics look good (win_rate >50%, FDR <15%), try *lowering* `radar_score_threshold` in [140, 170] to capture more trades without degrading quality. +## Research Directions -6. **Fee Drag Emergency**: If fees exceed gross PnL, simultaneously raise `radar_score_threshold` to [220, 280] and `pulse_confidence_threshold` to [85, 95]. +1. **High FDR (>30%)**: Raise `radar_score_threshold` toward 250 — filter low-quality entries. +2. **Low Win Rate (<40%)**: Raise `radar_score_threshold` toward 220 — require higher conviction. +3. **Too few trades**: Lower `radar_score_threshold` toward 140 — loosen entry criteria. +4. **Healthy metrics**: Try lowering `radar_score_threshold` toward 140 to capture more trades. +5. **Fee Drag Emergency**: Raise `radar_score_threshold` to [220, 280]. ## Workflow -1. Start with the current `apex_config.json` as baseline +1. Start with current `apex_config.json` as baseline 2. Run backtest to get baseline metrics -3. Pick a research direction based on the metrics -4. Modify one parameter at a time within bounds -5. Re-run backtest and compare -6. If `REJECT: too few trades` appears, the config is too restrictive — back off -7. Keep the config that maximizes `net_pnl` while passing all quality gates +3. Pick a direction based on the metrics +4. Change `radar_score_threshold` by one step (±10) +5. Re-run backtest and compare `net_pnl` +6. Keep if improved, revert if not +7. Repeat until no improvement found ## Quality Gates - Must produce at least 5 round trips -- `profit_factor` must be > 1.0 (net profitable) -- `fdr` must be < 50% (fees not destroying all edge) +- `profit_factor` must be > 1.0 +- `fdr` must be < 50% diff --git a/deploy/openclaw-railway/Dockerfile b/deploy/openclaw-railway/Dockerfile index 6978e3f..a0feff0 100644 --- a/deploy/openclaw-railway/Dockerfile +++ b/deploy/openclaw-railway/Dockerfile @@ -60,18 +60,18 @@ RUN mv /usr/bin/rg /usr/bin/rg-real \ # Install agent-cli (our trading CLI) WORKDIR /agent-cli -COPY ../../ . +COPY . . RUN pip install --no-cache-dir --break-system-packages -e ".[mcp]" # Wrapper app WORKDIR /app -COPY package.json ./ +COPY deploy/openclaw-railway/package.json ./ RUN npm install --production -COPY src ./src +COPY deploy/openclaw-railway/src ./src # Workspace defaults (copied to volume at runtime) -COPY workspace /opt/workspace-defaults +COPY deploy/openclaw-railway/workspace /opt/workspace-defaults # Vendor mcporter skill RUN set -eux; \ diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index 62e101c..b88927c 100644 --- a/deploy/openclaw-railway/src/bootstrap.mjs +++ b/deploy/openclaw-railway/src/bootstrap.mjs @@ -23,6 +23,15 @@ const PROVIDER_MAP = { blockrun: { key: "blockrun-wallet-key", provider: "blockrun" }, }; +function detectAuthMode(credentialValue, provider) { + // For Anthropic, check if it's an OAuth token (starts with sk-ant-oauth-) + // Otherwise assume API key authentication + if (provider === "anthropic" && credentialValue.startsWith("sk-ant-oauth-")) { + return "oauth"; + } + return "token"; +} + export async function bootstrap() { console.log("[bootstrap] Starting auto-configuration..."); @@ -48,7 +57,7 @@ export async function bootstrap() { } } - // 3. Generate openclaw.json with our MCP server + // 3. Generate openclaw.json with MCP server const config = buildConfig(); writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); console.log("[bootstrap] Generated openclaw.json"); @@ -84,39 +93,42 @@ function buildConfig() { ? (process.env.BLOCKRUN_WALLET_KEY || "") : aiKey; - const config = { - // Security (headless deployment) - deviceAuth: false, - insecureAuth: true, - - // Agent settings - agentConcurrency: 10, - subagentConcurrency: 12, - - // AI provider - provider: providerInfo.provider, - [providerInfo.key]: credentialValue, + const profileName = `${providerInfo.provider}:auto`; + const authMode = detectAuthMode(credentialValue, providerInfo.provider); - // MCP servers — our trading CLI is the primary tool provider - mcpServers: { - nunchi_trading: { - command: "python3", - args: ["-m", "cli.main", "mcp", "serve"], - cwd: "/agent-cli", - env: { - HL_PRIVATE_KEY: process.env.HL_PRIVATE_KEY || "", - HL_TESTNET: process.env.HL_TESTNET || "true", - ...(isBlockrun ? { - BLOCKRUN_WALLET_KEY: process.env.BLOCKRUN_WALLET_KEY || "", - BLOCKRUN_PROXY_PORT: process.env.BLOCKRUN_PROXY_PORT || "8402", - } : {}), + const config = { + gateway: { + mode: "local", + }, + agents: { + defaults: { + maxConcurrent: 10, + subagents: { maxConcurrent: 12 }, + }, + }, + auth: { + profiles: { + [profileName]: { + provider: providerInfo.provider, + mode: authMode, + }, + }, + }, + tools: { + mcp: { + servers: { + "nunchi-trading": { + command: "python3", + args: ["-m", "cli.main", "mcp", "serve", "--transport", "stdio"], + cwd: "/agent-cli", + env: { + ...process.env, + PYTHONPATH: "/agent-cli", + }, + }, }, }, }, - - // Workspace - workspaceDir: WORKSPACE_DIR, - stateDir: STATE_DIR, }; // Telegram integration @@ -124,9 +136,6 @@ function buildConfig() { config.channels = { telegram: { botToken: process.env.TELEGRAM_BOT_TOKEN, - allowedUsers: process.env.TELEGRAM_USERNAME - ? [process.env.TELEGRAM_USERNAME.replace("@", "")] - : [], }, }; } diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index 7699ef5..3bf13f6 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -10,6 +10,7 @@ const express = require("express"); const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); +const { spawn } = require("child_process"); const httpProxy = require("http-proxy"); const { bootstrap } = require("./bootstrap.mjs"); const { startGateway, waitForGatewayReady, getGatewayProcess } = require("./gateway"); @@ -24,6 +25,43 @@ const START_TIME = Date.now(); const AGENT_CLI_DIR = "/agent-cli"; const DATA_DIR = process.env.DATA_DIR || "/data"; +let mcpProcess = null; + +function startMCPServer() { + if (mcpProcess && !mcpProcess.killed) { + console.log("[mcp] Already running (pid=%d)", mcpProcess.pid); + return; + } + + console.log("[mcp] Starting MCP server..."); + + mcpProcess = spawn("python3", ["-m", "cli.main", "mcp", "serve", "--transport", "stdio"], { + cwd: AGENT_CLI_DIR, + env: { + ...process.env, + PYTHONPATH: AGENT_CLI_DIR, + }, + stdio: ["pipe", "pipe", "pipe"], + }); + + mcpProcess.stdout.on("data", (data) => { + const line = data.toString().trim(); + if (line) console.log(`[mcp] ${line}`); + }); + + mcpProcess.stderr.on("data", (data) => { + const line = data.toString().trim(); + if (line) console.error(`[mcp] ${line}`); + }); + + mcpProcess.on("exit", (code, signal) => { + console.log(`[mcp] Exited (code=${code}, signal=${signal})`); + mcpProcess = null; + }); + + console.log("[mcp] Spawned (pid=%d)", mcpProcess.pid); +} + // Proxy to OpenClaw gateway const proxy = httpProxy.createProxyServer({ target: `http://${GATEWAY_HOST}:${GATEWAY_PORT}`, @@ -46,6 +84,8 @@ app.get("/health", (req, res) => { uptime_s: Math.floor((Date.now() - START_TIME) / 1000), gateway_alive: gw ? !gw.killed : false, gateway_pid: gw ? gw.pid : null, + mcp_alive: mcpProcess ? !mcpProcess.killed : false, + mcp_pid: mcpProcess ? mcpProcess.pid : null, }); }); @@ -234,13 +274,38 @@ const server = app.listen(PORT, async () => { console.log(`[server] Listening on :${PORT}`); try { - // Step 1: Bootstrap (create dirs, sync workspace, generate configs) - await bootstrap(); + // Step 0: Wipe stale openclaw config so bootstrap regenerates it cleanly + const configPath = `${process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"}/openclaw.json`; + try { + fs.unlinkSync(configPath); + console.log("[server] Cleared stale openclaw.json"); + } catch { + // file may not exist yet + } - // Step 2: Auto-onboard if credentials present + // Step 1: Start MCP server first (so it's available when OpenClaw starts) + startMCPServer(); + await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for MCP server to start + + // Step 2: Auto-onboard first (it may overwrite config) await autoOnboard(); - // Step 3: Start OpenClaw gateway + // Step 3: Bootstrap after onboard so our config is the final one + await bootstrap(); + + // Step 4: Run doctor fix to clean up any invalid config keys + try { + execSync("openclaw doctor --fix", { + timeout: 30000, + stdio: "pipe", + env: { ...process.env, OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR || "/data/.openclaw" }, + }); + console.log("[server] OpenClaw doctor fix applied"); + } catch { + // best-effort + } + + // Step 5: Start OpenClaw gateway startGateway(); await waitForGatewayReady(); console.log("[server] OpenClaw gateway is ready"); @@ -264,6 +329,12 @@ function shutdown(signal) { if (!gw.killed) gw.kill("SIGKILL"); }, 10000); } + if (mcpProcess && !mcpProcess.killed) { + mcpProcess.kill("SIGTERM"); + setTimeout(() => { + if (!mcpProcess.killed) mcpProcess.kill("SIGKILL"); + }, 5000); + } server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 15000); } diff --git a/deploy/openclaw-render/README.md b/deploy/openclaw-render/README.md new file mode 100644 index 0000000..adc1dde --- /dev/null +++ b/deploy/openclaw-render/README.md @@ -0,0 +1,120 @@ +# Deploying Nunchi OpenClaw Agent to Render + +This guide helps you deploy the Nunchi trading agent as an OpenClaw agent on Render. + +## Prerequisites + +1. A Render account +2. Hyperliquid wallet private key +3. AI API key (Anthropic, OpenAI, etc.) +4. Optional: Telegram bot token for messaging + +## Deployment Steps + +### 1. Connect Repository to Render + +1. Go to [Render Dashboard](https://dashboard.render.com) +2. Click "New" → "Blueprint" +3. Connect your GitHub repository (`Nunchi-trade/agent-cli`) +4. Render will detect the `render.yaml` file + +### 2. Configure Environment Variables + +In the Render dashboard, set these environment variables as secrets: + +**Required:** +- `HL_PRIVATE_KEY`: Your Hyperliquid wallet private key +- `AI_API_KEY`: Your AI provider API key or OAuth token + - For Anthropic/Claude: API key (sk-ant-...) or OAuth token (sk-ant-oauth-...) + - For OpenAI: API key + - For Google/Gemini: API key + - For OpenRouter: API key + +**Optional:** +- `TELEGRAM_BOT_TOKEN`: For Telegram bot integration +- `HL_TESTNET`: Set to `false` for mainnet (default: `true` for testnet) +- `AI_PROVIDER`: `anthropic`, `openai`, `gemini`, etc. (default: `anthropic`) +- `RUN_MODE`: Trading mode - `apex`, `pulse`, `radar`, etc. (default: `apex`) +- `INSTRUMENT`: Trading instrument (default: `ETH-PERP`) + +### 3. Configure Persistent Disk + +The `render.yaml` includes a 10GB persistent disk mounted at `/data` for storing OpenClaw state and workspace data. + +### 4. Deploy + +Click "Create Blueprint" to deploy. The service will: +1. Build the Docker image +2. Run the bootstrap script to configure OpenClaw +3. Start the OpenClaw gateway +4. Serve the agent at the provided URL + +## Troubleshooting + +### Bot Not Responding + +If the bot doesn't reply to messages: + +1. **Check Health Endpoint**: Visit `https://your-service.onrender.com/health` + - Should return JSON with `status: "ok"` + - `gateway_alive: true` + - `mcp_alive: true` (MCP server should be running) + +2. **Check Logs**: In Render dashboard, view service logs + - Look for `[bootstrap] Configuration complete` + - Look for `[mcp] Starting MCP server...` + - Look for `[gateway] Starting OpenClaw gateway...` + - Look for successful MCP server connection + +3. **Verify Environment Variables**: + - `AI_API_KEY` must be set and valid + - `HL_PRIVATE_KEY` must be set + - Check AI provider compatibility + +4. **Test MCP Server Directly**: + ```bash + # SSH into your Render service or check logs for MCP server output + # Look for lines like "[mcp] Listening on port 18790" or similar + ``` + +5. **Test Trading Tools**: + ```bash + curl https://your-service.onrender.com/api/status + curl https://your-service.onrender.com/api/strategies + ``` + +### Common Issues + +- **MCP Server Not Starting**: Check if Python dependencies are installed correctly +- **OpenClaw Configuration**: The bootstrap script generates `openclaw.json` with MCP server config +- **Port Conflicts**: Ensure ports 18789 (gateway) and 18790 (MCP) are available +- **Memory Issues**: OpenClaw agents need sufficient memory (use at least 1GB RAM) + +### Manual Testing + +You can test the agent directly: + +```bash +# Check status +curl https://your-service.onrender.com/status + +# Check API status +curl https://your-service.onrender.com/api/status + +# Check strategies +curl https://your-service.onrender.com/api/strategies +``` + +## Architecture + +The deployment runs: +- **Express server** (port 10000) - Health checks and API endpoints +- **OpenClaw gateway** (internal port 18789) - Agent runtime +- **Nunchi MCP server** - Trading tools and strategies +- **Persistent storage** (/data) - Configuration and workspace + +## Security Notes + +- Never commit private keys to the repository +- Use Render's secret management for sensitive environment variables +- The agent has access to trading functions - monitor activity closely \ No newline at end of file diff --git a/modules/apex_config.py b/modules/apex_config.py index b4c0e37..372f735 100644 --- a/modules/apex_config.py +++ b/modules/apex_config.py @@ -17,18 +17,18 @@ class ApexConfig: margin_per_slot: float = 0.0 # auto-computed # Entry thresholds - radar_score_threshold: int = 170 + radar_score_threshold: int = 160 pulse_immediate_auto_entry: bool = True pulse_confidence_threshold: float = 70.0 # Exit parameters - conviction_collapse_minutes: int = 30 + conviction_collapse_minutes: int = 45 stagnation_minutes: int = 60 stagnation_min_roe: float = 3.0 max_negative_roe: float = -5.0 # Rotation cooldown - min_hold_ms: int = 2_700_000 # 45 min — blocks conviction/stagnation exits + min_hold_ms: int = 900_000 # 15 min — blocks conviction/stagnation exits slot_cooldown_ms: int = 300_000 # 5 min — prevents slot reuse after close # Risk @@ -46,7 +46,7 @@ class ApexConfig: # Tick schedule tick_interval_s: float = 60.0 - radar_interval_ticks: int = 15 + radar_interval_ticks: int = 10 watchdog_interval_ticks: int = 5 # REFLECT self-improvement diff --git a/modules/apex_engine.py b/modules/apex_engine.py index 770dc9f..777fac7 100644 --- a/modules/apex_engine.py +++ b/modules/apex_engine.py @@ -87,7 +87,11 @@ def evaluate( if exit_action: actions.append(exit_action) - # 3. Entry evaluation + # 3. Consecutive loss cooldown gate + if state.cooldown_until_ms > 0 and now_ms < state.cooldown_until_ms: + return actions # skip entries, only exits above + + # 4. Entry evaluation entry_actions = self._evaluate_entries( state, pulse_signals, radar_opps, now_ms, smart_money_signals=smart_money_signals or [], diff --git a/modules/apex_state.py b/modules/apex_state.py index 12ebfce..8c1fdd0 100644 --- a/modules/apex_state.py +++ b/modules/apex_state.py @@ -63,6 +63,8 @@ class ApexState: daily_loss_triggered: bool = False total_trades: int = 0 total_pnl: float = 0.0 + consecutive_losses: int = 0 + cooldown_until_ms: int = 0 entry_queue: List[Dict[str, Any]] = field(default_factory=list) def get_empty_slot(self, now_ms: int = 0, cooldown_ms: int = 0) -> Optional[ApexSlot]: @@ -93,6 +95,8 @@ def to_dict(self) -> Dict[str, Any]: "daily_loss_triggered": self.daily_loss_triggered, "total_trades": self.total_trades, "total_pnl": self.total_pnl, + "consecutive_losses": self.consecutive_losses, + "cooldown_until_ms": self.cooldown_until_ms, "entry_queue": self.entry_queue, } @@ -105,6 +109,8 @@ def from_dict(cls, d: Dict[str, Any]) -> "ApexState": daily_loss_triggered=d.get("daily_loss_triggered", False), total_trades=d.get("total_trades", 0), total_pnl=d.get("total_pnl", 0.0), + consecutive_losses=d.get("consecutive_losses", 0), + cooldown_until_ms=d.get("cooldown_until_ms", 0), entry_queue=d.get("entry_queue", []), ) state.slots = [ApexSlot.from_dict(s) for s in d.get("slots", [])] diff --git a/modules/guard_config.py b/modules/guard_config.py index 1c659d9..aacf874 100644 --- a/modules/guard_config.py +++ b/modules/guard_config.py @@ -46,9 +46,9 @@ class GuardConfig: phase1_retrace: float = 0.03 # 3% retrace from high water phase1_max_breaches: int = 3 # Consecutive breaches before close phase1_absolute_floor: float = 0.0 # Hard price floor (0 = disabled) - phase1_max_duration_ms: int = 5_400_000 # 90 min max in Phase 1 (0 = disabled) - phase1_weak_peak_ms: int = 2_700_000 # 45 min weak-peak check (0 = disabled) - phase1_weak_peak_min_roe: float = 3.0 # Min peak ROE% to survive weak-peak check + phase1_max_duration_ms: int = 10_800_000 # 180 min max in Phase 1 (0 = disabled) + phase1_weak_peak_ms: int = 5_400_000 # 90 min weak-peak check (0 = disabled) + phase1_weak_peak_min_roe: float = 1.5 # Min peak ROE% to survive weak-peak check # Phase 2: "Lock the bag" phase2_retrace: float = 0.015 # 1.5% default retrace @@ -143,9 +143,10 @@ def _register_presets() -> None: phase2_max_breaches=2, breach_decay_mode="hard", tiers=[ - Tier(trigger_pct=10.0, lock_pct=5.0, max_breaches=3), - Tier(trigger_pct=20.0, lock_pct=13.0, retrace=0.012, max_breaches=2), - Tier(trigger_pct=40.0, lock_pct=30.0, retrace=0.010, max_breaches=2), + Tier(trigger_pct=5.0, lock_pct=2.0, max_breaches=3), + Tier(trigger_pct=10.0, lock_pct=7.0, max_breaches=2), + Tier(trigger_pct=20.0, lock_pct=15.0, retrace=0.012, max_breaches=2), + Tier(trigger_pct=40.0, lock_pct=32.0, retrace=0.010, max_breaches=2), Tier(trigger_pct=75.0, lock_pct=64.0, retrace=0.006, max_breaches=1), ], stagnation_enabled=True, diff --git a/modules/radar_engine.py b/modules/radar_engine.py index 2a6b3a5..4a73e68 100644 --- a/modules/radar_engine.py +++ b/modules/radar_engine.py @@ -135,13 +135,21 @@ def _btc_macro( if ema5 and ema13 and ema13[-1] != 0: diff_pct = (ema5[-1] - ema13[-1]) / ema13[-1] * 100 - # 1h change + # 1h change (last candle) closes_1h = [float(c["c"]) for c in btc_1h] chg1h = 0.0 if len(closes_1h) >= 2 and closes_1h[-2] != 0: chg1h = (closes_1h[-1] - closes_1h[-2]) / closes_1h[-2] * 100 - # Classify + # 4h change from recent 1h candles (last 4 candles) + chg4h = 0.0 + if len(closes_1h) >= 5 and closes_1h[-5] != 0: + chg4h = (closes_1h[-1] - closes_1h[-5]) / closes_1h[-5] * 100 + + # Structural 1h trend for deterioration check + hourly_trend = classify_hourly_trend(btc_1h) + + # Classify base trend from 4h EMAs if diff_pct > 1.0: trend = "strong_up" elif diff_pct > 0.2: @@ -153,16 +161,28 @@ def _btc_macro( else: trend = "neutral" + # Short-term deterioration override: if 4h EMAs say bullish but + # structural 1h trend is not UP and recent 4h price change is + # negative, downgrade macro to neutral so stale uptrend doesn't + # keep boosting longs into a declining market + effective_trend = trend + if trend in ("up", "strong_up") and chg4h < 0 and hourly_trend != "UP": + effective_trend = "neutral" + elif trend in ("down", "strong_down") and chg4h > 0 and hourly_trend != "DOWN": + effective_trend = "neutral" + strength = min(int(abs(diff_pct) * 20), 100) return { "trend": trend, + "effective_trend": effective_trend, "strength": strength, "ema5": round(ema5[-1], 2) if ema5 else 0, "ema13": round(ema13[-1], 2) if ema13 else 0, "diff_pct": round(diff_pct, 3), "chg1h": round(chg1h, 3), - "modifiers": self.config.macro_modifiers.get(trend, {}), + "chg4h": round(chg4h, 3), + "modifiers": self.config.macro_modifiers.get(effective_trend, {}), } def _bulk_screen(self, all_markets: list) -> List[AssetMeta]: diff --git a/modules/radar_guard.py b/modules/radar_guard.py index 7401452..e2706c8 100644 --- a/modules/radar_guard.py +++ b/modules/radar_guard.py @@ -55,6 +55,9 @@ def scan( # Log summary stats = result.stats + btc_trend = result.btc_macro.get("trend", "?") + effective = result.btc_macro.get("effective_trend", btc_trend) + trend_label = btc_trend if btc_trend == effective else f"{btc_trend}→{effective}" log.info( "Scan complete: %d assets → %d stage1 → %d deep → %d qualified " "(%.1fs, BTC %s)", @@ -63,7 +66,7 @@ def scan( stats.get("deep_dived", 0), stats.get("qualified", 0), stats.get("scan_duration_ms", 0) / 1000, - result.btc_macro.get("trend", "?"), + trend_label, ) for opp in result.opportunities[:5]: diff --git a/parent/hl_proxy.py b/parent/hl_proxy.py index 10483ff..4055cdf 100644 --- a/parent/hl_proxy.py +++ b/parent/hl_proxy.py @@ -272,7 +272,8 @@ def _ensure_client(self): _patch_spot_meta_indexing() base_url = constants.TESTNET_API_URL if self.testnet else constants.MAINNET_API_URL - perp_dexs = [""] + list(HIP3_DEXS.keys()) + # HIP-3 DEXs (e.g. yex) only exist on testnet + perp_dexs = [""] + (list(HIP3_DEXS.keys()) if self.testnet else []) self._info = Info(base_url, skip_ws=True, timeout=10, perp_dexs=perp_dexs) account = Account.from_key(self.private_key) @@ -287,8 +288,8 @@ def _ensure_client(self): self._exchange = Exchange(account, base_url, perp_dexs=perp_dexs) log.info("HL client initialized: %s (testnet=%s)", self._address, self.testnet) - # Enable HIP-3 DEX abstraction for agent trading - if HIP3_DEXS: + # Enable HIP-3 DEX abstraction for agent trading (testnet only) + if HIP3_DEXS and self.testnet: try: self._exchange.agent_enable_dex_abstraction() log.info("HIP-3 DEX abstraction enabled") diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..b1c2b9f --- /dev/null +++ b/render.yaml @@ -0,0 +1,46 @@ +services: + - type: web + name: nunchi-openclaw-agent + runtime: docker + dockerfilePath: ./deploy/openclaw-railway/Dockerfile + dockerContext: . + plan: starter + healthCheckPath: /health + envVars: + - key: PORT + value: 10000 # Render uses 10000 by default + - key: HL_TESTNET + value: true + - key: APEX_PRESET + value: default + - key: RUN_MODE + value: apex + - key: INSTRUMENT + value: ETH-PERP + - key: DATA_DIR + value: /data + - key: OPENCLAW_STATE_DIR + value: /data/.openclaw + - key: OPENCLAW_WORKSPACE_DIR + value: /data/workspace + - key: INTERNAL_GATEWAY_HOST + value: 127.0.0.1 + - key: INTERNAL_GATEWAY_PORT + value: 18789 + - key: MCP_PORT + value: 18790 + - key: AI_PROVIDER + value: anthropic + - key: NODE_ENV + value: production + # Required secrets (set these in Render dashboard) + - key: HL_PRIVATE_KEY + sync: false # This will be set as a secret + - key: AI_API_KEY + sync: false # API key or OAuth token (for Claude: sk-ant-* or sk-ant-oauth-*) + - key: TELEGRAM_BOT_TOKEN + sync: false # Optional, for Telegram integration + disk: + name: nunchi-data + mountPath: /data + sizeGB: 10 \ No newline at end of file diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index dc6d4a8..5bed956 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -265,8 +265,11 @@ def build_command() -> list[str]: if mode in ("apex", "wolf"): cmd = py + ["apex", "run"] + config = os.environ.get("APEX_CONFIG") + if config: + cmd += ["--config", config] preset = os.environ.get("APEX_PRESET") - if preset: + if preset and not config: cmd += ["--preset", preset] budget = os.environ.get("APEX_BUDGET") if budget: diff --git a/skills/apex/scripts/standalone_runner.py b/skills/apex/scripts/standalone_runner.py index 2d8e251..fa1fc12 100644 --- a/skills/apex/scripts/standalone_runner.py +++ b/skills/apex/scripts/standalone_runner.py @@ -251,6 +251,30 @@ def _preflight_check(self) -> None: except Exception as e: log.warning("Preflight balance check failed: %s (continuing anyway)", e) + def _approve_builder_fee(self) -> None: + """Auto-approve builder fee on HL so orders with fees aren't rejected.""" + if not self.builder: + return + try: + from cli.builder_fee import BuilderFeeConfig + cfg = BuilderFeeConfig() + if not cfg.enabled: + return + exchange = getattr(self.hl, "_exchange", None) + if exchange is None: + exchange = getattr( + getattr(self.hl, "_hl", None), "_exchange", None) + if exchange is None: + return + result = exchange.approve_builder_fee( + cfg.builder_address, cfg.max_fee_rate_str) + log.info( + "Builder fee approved: %s (%s)", + cfg.builder_address, result) + except Exception as e: + log.warning( + "Builder fee approval failed (orders may reject): %s", e) + def run(self, max_ticks: int = 0) -> None: """Main loop. Blocks until max_ticks reached or SIGINT.""" self._running = True @@ -258,6 +282,7 @@ def run(self, max_ticks: int = 0) -> None: signal.signal(signal.SIGTERM, self._handle_shutdown) self._preflight_check() + self._approve_builder_fee() # Register with telemetry service if self.telemetry: @@ -888,6 +913,10 @@ def _execute_enter(self, action: ApexAction) -> None: size = (self.config.margin_per_slot * self.config.leverage) / mid side = "buy" if action.direction == "long" else "sell" + # Set exchange-level leverage to match config before entry + if hasattr(self.hl, "set_leverage"): + self.hl.set_leverage(int(self.config.leverage), coin) + # Entry order type: directional strategies use IOC (need immediate fills # on fast-moving assets), pulse/radar use configured default (ALO for rebates) is_directional = action.source not in ("pulse_immediate", "pulse_signal", "radar") @@ -1009,6 +1038,22 @@ def _close_slot(self, slot: ApexSlot, reason: str, pnl: float) -> None: self.state.daily_pnl += pnl self.state.total_pnl += pnl + # Track consecutive losses and trigger cooldown + if pnl < 0: + self.state.consecutive_losses += 1 + if self.state.consecutive_losses >= self.config.cooldown_trigger_losses: + now_ms = int(time.time() * 1000) + self.state.cooldown_until_ms = ( + now_ms + self.config.cooldown_duration_ms) + log.warning( + "LOSS COOLDOWN: %d consecutive losses, " + "blocking entries for %d min", + self.state.consecutive_losses, + self.config.cooldown_duration_ms // 60_000) + else: + self.state.consecutive_losses = 0 + self.state.cooldown_until_ms = 0 + if self.state.daily_pnl <= -self.config.daily_loss_limit: self.state.daily_loss_triggered = True log.warning("DAILY LOSS LIMIT triggered: $%.2f", self.state.daily_pnl) diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md index 557fb1c..314815c 100644 --- a/skills/onboard/SKILL.md +++ b/skills/onboard/SKILL.md @@ -300,7 +300,8 @@ 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 | +| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy (API key) | +| `ANTHROPIC_SESSION_TOKEN` | No | For `claude_agent` strategy (Claude Max session token) | | `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | \* Either keystore with `HL_KEYSTORE_PASSWORD` or `HL_PRIVATE_KEY` is required. diff --git a/strategies/claude_agent.py b/strategies/claude_agent.py index cb4a8ff..cbe1534 100644 --- a/strategies/claude_agent.py +++ b/strategies/claude_agent.py @@ -9,7 +9,11 @@ hl run claude_agent --mock --max-ticks 5 --tick 15 hl run claude_agent -i ETH-PERP --tick 15 - # Claude + # Claude (with API key) + hl run claude_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001 + + # Claude Max (with session token) + export ANTHROPIC_SESSION_TOKEN=your_session_token_here hl run claude_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001 # Gemini Flash @@ -171,10 +175,23 @@ def _get_anthropic_client(self): raise ImportError( "anthropic package required. Install: pip3 install anthropic" ) - api_key = os.environ.get("ANTHROPIC_API_KEY") - if not api_key: - raise ValueError("ANTHROPIC_API_KEY environment variable required") - self._anthropic_client = anthropic.Anthropic(api_key=api_key) + + # Try session token first (for Claude Max), then API key + auth_token = os.environ.get("ANTHROPIC_SESSION_TOKEN") or os.environ.get("ANTHROPIC_API_KEY") + if not auth_token: + raise ValueError("ANTHROPIC_SESSION_TOKEN or ANTHROPIC_API_KEY environment variable required") + + # For session tokens, we need to use a different approach + if os.environ.get("ANTHROPIC_SESSION_TOKEN"): + # Session tokens are used differently - they might need custom headers or different endpoint + # This is a simplified approach - you may need to adjust based on Claude Max's auth method + self._anthropic_client = anthropic.Anthropic( + api_key=auth_token, + # Add any additional configuration needed for session tokens + ) + else: + # Standard API key authentication + self._anthropic_client = anthropic.Anthropic(api_key=auth_token) return self._anthropic_client def _get_gemini_client(self): diff --git a/strategies/simplified_ensemble.py b/strategies/simplified_ensemble.py index 45dcbc7..876cbc8 100644 --- a/strategies/simplified_ensemble.py +++ b/strategies/simplified_ensemble.py @@ -37,7 +37,7 @@ ATR_STOP_MULT = 5.5 BASE_POSITION_PCT = 0.08 MIN_VOTES = 4 -COOLDOWN_BARS = 2 +COOLDOWN_BARS = 120 # 2 hours at 60s ticks — prevents fee churn on re-entry # Minimum ticks needed before signals are valid MIN_HISTORY = max(MACD_SLOW + MACD_SIGNAL + 5, EMA_SLOW + 10, VOL_LOOKBACK, ATR_LOOKBACK, BB_PERIOD * 3) + 1 diff --git a/tests/test_trailing_stop.py b/tests/test_trailing_stop.py index cf24d78..fa2bd9e 100644 --- a/tests/test_trailing_stop.py +++ b/tests/test_trailing_stop.py @@ -532,9 +532,9 @@ def test_moderate_preset_exists(self): def test_tight_preset_exists(self): cfg = PRESETS["tight"] - assert len(cfg.tiers) == 4 + assert len(cfg.tiers) == 5 assert cfg.stagnation_enabled is True - assert cfg.tiers[3].max_breaches == 1 + assert cfg.tiers[4].max_breaches == 1 # ---------------------------------------------------------------------------