From 27ccdd4285e5f73ec5f3dfff9794a1a183e7db60 Mon Sep 17 00:00:00 2001 From: bhargav55 Date: Thu, 2 Apr 2026 18:05:10 +0530 Subject: [PATCH 01/17] mainnet changes --- README.md | 5 ++-- cli/commands/setup.py | 6 ++-- cli/skill.md | 3 +- configs/apex_btc_mainnet.yaml | 16 +++++++++++ configs/autoresearch_program.md | 50 ++++++++++++++------------------- parent/hl_proxy.py | 7 +++-- skills/onboard/SKILL.md | 3 +- strategies/claude_agent.py | 27 ++++++++++++++---- 8 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 configs/apex_btc_mainnet.yaml 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/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/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..6bd4305 --- /dev/null +++ b/configs/apex_btc_mainnet.yaml @@ -0,0 +1,16 @@ +# 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: 170 + +# Scaled for $50 account +total_budget: 50.0 +max_slots: 1 +leverage: 5.0 +daily_loss_limit: 10.0 +guard_preset: tight 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/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/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): From eee409d44b008236e023aac0f77e929145199e38 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 18:58:25 +0530 Subject: [PATCH 02/17] use config changes --- cli/hl_adapter.py | 4 ++-- scripts/entrypoint.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/hl_adapter.py b/cli/hl_adapter.py index f8b898d..a564fd3 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, 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: From 12ad1c8b8132414d13e6bbd5f932b289a14d77fc Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 20:11:47 +0530 Subject: [PATCH 03/17] add cooldown --- configs/apex_btc_mainnet.yaml | 1 + strategies/simplified_ensemble.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/configs/apex_btc_mainnet.yaml b/configs/apex_btc_mainnet.yaml index 6bd4305..f3e61ec 100644 --- a/configs/apex_btc_mainnet.yaml +++ b/configs/apex_btc_mainnet.yaml @@ -14,3 +14,4 @@ max_slots: 1 leverage: 5.0 daily_loss_limit: 10.0 guard_preset: tight +slot_cooldown_ms: 7200000 # 2 hours — prevents fee churn on re-entry 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 From c0f40221d3876def1b06102b0d2741a0dffd40a0 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 21:13:51 +0530 Subject: [PATCH 04/17] change docker file --- deploy/openclaw-railway/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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; \ From 307a78f7389f1189d9cb1fca2dc26c06e7e0759e Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 21:23:37 +0530 Subject: [PATCH 05/17] Run openclaw doctor fix before gateway start, update config schema --- deploy/openclaw-railway/src/bootstrap.mjs | 63 +++++++++++------------ deploy/openclaw-railway/src/server.js | 14 ++++- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index 62e101c..de8e994 100644 --- a/deploy/openclaw-railway/src/bootstrap.mjs +++ b/deploy/openclaw-railway/src/bootstrap.mjs @@ -85,38 +85,40 @@ function buildConfig() { : aiKey; const config = { - // Security (headless deployment) - deviceAuth: false, - insecureAuth: true, - - // Agent settings - agentConcurrency: 10, - subagentConcurrency: 12, - - // AI provider - provider: providerInfo.provider, - [providerInfo.key]: credentialValue, - - // 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", - } : {}), + auth: { + profiles: { + "primary": { + provider: providerInfo.provider, + [providerInfo.key]: credentialValue, }, }, }, - - // Workspace - workspaceDir: WORKSPACE_DIR, - stateDir: STATE_DIR, + agents: { + defaults: { + maxConcurrent: 10, + subagents: { maxConcurrent: 12 }, + }, + }, + mcp: { + servers: { + 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", + } : {}), + }, + }, + }, + }, + workspace: { + dir: WORKSPACE_DIR, + }, }; // Telegram integration @@ -124,9 +126,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..6b94007 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -240,7 +240,19 @@ const server = app.listen(PORT, async () => { // Step 2: Auto-onboard if credentials present await autoOnboard(); - // Step 3: Start OpenClaw gateway + // Step 3: 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 4: Start OpenClaw gateway startGateway(); await waitForGatewayReady(); console.log("[server] OpenClaw gateway is ready"); From 3406b8794236e5756aa8ccc1eef136debbe074d8 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 22:04:24 +0530 Subject: [PATCH 06/17] Use token mode for openclaw auth profile --- deploy/openclaw-railway/src/bootstrap.mjs | 32 +++++------------------ deploy/openclaw-railway/src/server.js | 9 +++++++ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index de8e994..282a66a 100644 --- a/deploy/openclaw-railway/src/bootstrap.mjs +++ b/deploy/openclaw-railway/src/bootstrap.mjs @@ -84,41 +84,23 @@ function buildConfig() { ? (process.env.BLOCKRUN_WALLET_KEY || "") : aiKey; + const profileName = `${providerInfo.provider}:auto`; + const config = { - auth: { - profiles: { - "primary": { - provider: providerInfo.provider, - [providerInfo.key]: credentialValue, - }, - }, - }, agents: { defaults: { maxConcurrent: 10, subagents: { maxConcurrent: 12 }, }, }, - mcp: { - servers: { - 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", - } : {}), - }, + auth: { + profiles: { + [profileName]: { + provider: providerInfo.provider, + mode: "token", }, }, }, - workspace: { - dir: WORKSPACE_DIR, - }, }; // Telegram integration diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index 6b94007..8df1ae5 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -234,6 +234,15 @@ const server = app.listen(PORT, async () => { console.log(`[server] Listening on :${PORT}`); try { + // 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 1: Bootstrap (create dirs, sync workspace, generate configs) await bootstrap(); From 4d5c4ae751d9607d3c233a04031e8370acbf8c3f Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 2 Apr 2026 23:10:17 +0530 Subject: [PATCH 07/17] Run bootstrap after onboard so telegram config is not overwritten --- deploy/openclaw-railway/src/server.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index 8df1ae5..91b5729 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -243,12 +243,12 @@ const server = app.listen(PORT, async () => { // file may not exist yet } - // Step 1: Bootstrap (create dirs, sync workspace, generate configs) - await bootstrap(); - - // Step 2: Auto-onboard if credentials present + // Step 1: Auto-onboard first (it may overwrite config) await autoOnboard(); + // Step 2: Bootstrap after onboard so our config is the final one + await bootstrap(); + // Step 3: Run doctor fix to clean up any invalid config keys try { execSync("openclaw doctor --fix", { @@ -261,7 +261,21 @@ const server = app.listen(PORT, async () => { // best-effort } - // Step 4: Start OpenClaw gateway + // Step 4: Register MCP server with OpenClaw + try { + const stateDir = process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"; + const hlKey = process.env.HL_PRIVATE_KEY || ""; + const hlTestnet = process.env.HL_TESTNET || "true"; + execSync( + `openclaw mcp add nunchi_trading --command "python3 -m cli.main mcp serve" --cwd /agent-cli --env HL_PRIVATE_KEY=${hlKey} --env HL_TESTNET=${hlTestnet}`, + { timeout: 30000, stdio: "pipe", env: { ...process.env, OPENCLAW_STATE_DIR: stateDir } } + ); + console.log("[server] MCP server registered"); + } catch (err) { + console.warn("[server] MCP registration failed (may already exist):", err.message); + } + + // Step 5: Start OpenClaw gateway startGateway(); await waitForGatewayReady(); console.log("[server] OpenClaw gateway is ready"); From 503b4329c037a0de6bd87a1b3bbd4563ef0f002f Mon Sep 17 00:00:00 2001 From: simp-son Date: Fri, 3 Apr 2026 13:54:26 +0530 Subject: [PATCH 08/17] Add gateway.mode=local to config, remove invalid mcp command --- deploy/openclaw-railway/src/bootstrap.mjs | 3 +++ deploy/openclaw-railway/src/server.js | 16 +--------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index 282a66a..7d8a56f 100644 --- a/deploy/openclaw-railway/src/bootstrap.mjs +++ b/deploy/openclaw-railway/src/bootstrap.mjs @@ -87,6 +87,9 @@ function buildConfig() { const profileName = `${providerInfo.provider}:auto`; const config = { + gateway: { + mode: "local", + }, agents: { defaults: { maxConcurrent: 10, diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index 91b5729..bc74fa9 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -261,21 +261,7 @@ const server = app.listen(PORT, async () => { // best-effort } - // Step 4: Register MCP server with OpenClaw - try { - const stateDir = process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"; - const hlKey = process.env.HL_PRIVATE_KEY || ""; - const hlTestnet = process.env.HL_TESTNET || "true"; - execSync( - `openclaw mcp add nunchi_trading --command "python3 -m cli.main mcp serve" --cwd /agent-cli --env HL_PRIVATE_KEY=${hlKey} --env HL_TESTNET=${hlTestnet}`, - { timeout: 30000, stdio: "pipe", env: { ...process.env, OPENCLAW_STATE_DIR: stateDir } } - ); - console.log("[server] MCP server registered"); - } catch (err) { - console.warn("[server] MCP registration failed (may already exist):", err.message); - } - - // Step 5: Start OpenClaw gateway + // Step 4: Start OpenClaw gateway startGateway(); await waitForGatewayReady(); console.log("[server] OpenClaw gateway is ready"); From b3e01f2455d30f4921bc465a09a482b849ab78e3 Mon Sep 17 00:00:00 2001 From: simp-son Date: Fri, 3 Apr 2026 17:23:43 +0530 Subject: [PATCH 09/17] add render config --- cli/commands/mcp.py | 9 +- deploy/openclaw-railway/src/bootstrap.mjs | 23 ++++- deploy/openclaw-railway/src/server.js | 55 +++++++++- deploy/openclaw-render/README.md | 120 ++++++++++++++++++++++ render.yaml | 46 +++++++++ 5 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 deploy/openclaw-render/README.md create mode 100644 render.yaml diff --git a/cli/commands/mcp.py b/cli/commands/mcp.py index 29acef9..da62443 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"), ): """Start MCP server exposing trading tools for AI agents.""" project_root = str(Path(__file__).resolve().parent.parent.parent) @@ -26,5 +28,8 @@ def mcp_serve( raise typer.Exit(1) server = create_mcp_server() - typer.echo(f"Starting MCP server (transport={transport}) ...") - server.run(transport=transport) + typer.echo(f"Starting MCP server (transport={transport}, port={port}) ...") + if transport == "sse": + server.run(transport=transport, port=port) + else: + server.run(transport=transport) diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index 7d8a56f..3c52e89 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"); @@ -85,6 +94,7 @@ function buildConfig() { : aiKey; const profileName = `${providerInfo.provider}:auto`; + const authMode = detectAuthMode(credentialValue, providerInfo.provider); const config = { gateway: { @@ -100,7 +110,16 @@ function buildConfig() { profiles: { [profileName]: { provider: providerInfo.provider, - mode: "token", + mode: authMode, + }, + }, + }, + tools: { + mcp: { + servers: { + "nunchi-trading": { + url: `http://127.0.0.1:${process.env.MCP_PORT || "18790"}/sse`, + }, }, }, }, diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index bc74fa9..41097ad 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"); @@ -20,10 +21,49 @@ const app = express(); const PORT = parseInt(process.env.PORT || "8080", 10); const GATEWAY_HOST = process.env.INTERNAL_GATEWAY_HOST || "127.0.0.1"; const GATEWAY_PORT = parseInt(process.env.INTERNAL_GATEWAY_PORT || "18789", 10); +const MCP_PORT = parseInt(process.env.MCP_PORT || "18790", 10); 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", "sse", "--port", String(MCP_PORT)], { + cwd: AGENT_CLI_DIR, + env: { + ...process.env, + MCP_PORT: String(MCP_PORT), + PYTHONPATH: AGENT_CLI_DIR, + }, + stdio: ["ignore", "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 +86,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, }); }); @@ -249,7 +291,10 @@ const server = app.listen(PORT, async () => { // Step 2: Bootstrap after onboard so our config is the final one await bootstrap(); - // Step 3: Run doctor fix to clean up any invalid config keys + // Step 3: Start MCP server + startMCPServer(); + + // Step 4: Run doctor fix to clean up any invalid config keys try { execSync("openclaw doctor --fix", { timeout: 30000, @@ -261,7 +306,7 @@ const server = app.listen(PORT, async () => { // best-effort } - // Step 4: Start OpenClaw gateway + // Step 5: Start OpenClaw gateway startGateway(); await waitForGatewayReady(); console.log("[server] OpenClaw gateway is ready"); @@ -285,6 +330,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/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 From de42f2f9085b0307990898b5a450ce7dcebbfa17 Mon Sep 17 00:00:00 2001 From: simp-son Date: Fri, 3 Apr 2026 17:30:54 +0530 Subject: [PATCH 10/17] Fix OpenClaw agent MCP integration - command-based MCP server, OAuth support, startup sequence --- cli/commands/mcp.py | 9 +++------ deploy/openclaw-railway/src/bootstrap.mjs | 8 +++++++- deploy/openclaw-railway/src/server.js | 17 ++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cli/commands/mcp.py b/cli/commands/mcp.py index da62443..7dc7726 100644 --- a/cli/commands/mcp.py +++ b/cli/commands/mcp.py @@ -14,7 +14,7 @@ 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"), + 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) @@ -28,8 +28,5 @@ def mcp_serve( raise typer.Exit(1) server = create_mcp_server() - typer.echo(f"Starting MCP server (transport={transport}, port={port}) ...") - if transport == "sse": - server.run(transport=transport, port=port) - else: - server.run(transport=transport) + typer.echo(f"Starting MCP server (transport={transport}) ...") + server.run(transport=transport) diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs index 3c52e89..b88927c 100644 --- a/deploy/openclaw-railway/src/bootstrap.mjs +++ b/deploy/openclaw-railway/src/bootstrap.mjs @@ -118,7 +118,13 @@ function buildConfig() { mcp: { servers: { "nunchi-trading": { - url: `http://127.0.0.1:${process.env.MCP_PORT || "18790"}/sse`, + command: "python3", + args: ["-m", "cli.main", "mcp", "serve", "--transport", "stdio"], + cwd: "/agent-cli", + env: { + ...process.env, + PYTHONPATH: "/agent-cli", + }, }, }, }, diff --git a/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js index 41097ad..3bf13f6 100644 --- a/deploy/openclaw-railway/src/server.js +++ b/deploy/openclaw-railway/src/server.js @@ -21,7 +21,6 @@ const app = express(); const PORT = parseInt(process.env.PORT || "8080", 10); const GATEWAY_HOST = process.env.INTERNAL_GATEWAY_HOST || "127.0.0.1"; const GATEWAY_PORT = parseInt(process.env.INTERNAL_GATEWAY_PORT || "18789", 10); -const MCP_PORT = parseInt(process.env.MCP_PORT || "18790", 10); const START_TIME = Date.now(); const AGENT_CLI_DIR = "/agent-cli"; const DATA_DIR = process.env.DATA_DIR || "/data"; @@ -36,14 +35,13 @@ function startMCPServer() { console.log("[mcp] Starting MCP server..."); - mcpProcess = spawn("python3", ["-m", "cli.main", "mcp", "serve", "--transport", "sse", "--port", String(MCP_PORT)], { + mcpProcess = spawn("python3", ["-m", "cli.main", "mcp", "serve", "--transport", "stdio"], { cwd: AGENT_CLI_DIR, env: { ...process.env, - MCP_PORT: String(MCP_PORT), PYTHONPATH: AGENT_CLI_DIR, }, - stdio: ["ignore", "pipe", "pipe"], + stdio: ["pipe", "pipe", "pipe"], }); mcpProcess.stdout.on("data", (data) => { @@ -285,15 +283,16 @@ const server = app.listen(PORT, async () => { // file may not exist yet } - // Step 1: Auto-onboard first (it may overwrite config) + // 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 2: Bootstrap after onboard so our config is the final one + // Step 3: Bootstrap after onboard so our config is the final one await bootstrap(); - // Step 3: Start MCP server - startMCPServer(); - // Step 4: Run doctor fix to clean up any invalid config keys try { execSync("openclaw doctor --fix", { From d70bbebafed199f8f23f6092561ad8cc88d95d20 Mon Sep 17 00:00:00 2001 From: simp-son Date: Mon, 6 Apr 2026 18:20:49 +0530 Subject: [PATCH 11/17] tune lower radar threshold to 160, scan every 10 ticks, extend conviction window to 45min --- modules/apex_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/apex_config.py b/modules/apex_config.py index b4c0e37..7814b51 100644 --- a/modules/apex_config.py +++ b/modules/apex_config.py @@ -17,12 +17,12 @@ 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 @@ -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 From b73bd19e154998674d3ebe2255f95cf95a437aba Mon Sep 17 00:00:00 2001 From: simp-son Date: Mon, 6 Apr 2026 19:37:37 +0530 Subject: [PATCH 12/17] Fix trigger SL rejected with invalid price on HL Trigger price was not being rounded to HL's 5-sig-fig tick size before placing stop-loss orders. For BTC at ~$69k the tick is 1.0, so a floor like 65896.75 was rejected as invalid. Round trigger_price the same way place_order already rounds limit prices. Co-Authored-By: Claude Sonnet 4.6 --- cli/hl_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/hl_adapter.py b/cli/hl_adapter.py index a564fd3..620b03a 100644 --- a/cli/hl_adapter.py +++ b/cli/hl_adapter.py @@ -508,6 +508,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, From d5d52f71f9128a1f8d632575088ec9f13db380a6 Mon Sep 17 00:00:00 2001 From: simp-son Date: Mon, 6 Apr 2026 19:49:26 +0530 Subject: [PATCH 13/17] Set exchange leverage to config value before entry, fix stale radar threshold Standalone runner never called set_leverage on HL, so BTC account stayed at 20x (default/prior setting). Now sets leverage to config.leverage (5x) right before each entry order. Also sync apex_btc_mainnet.yaml radar_score_threshold to 160 (tuned last session). Co-Authored-By: Claude Sonnet 4.6 --- configs/apex_btc_mainnet.yaml | 2 +- skills/apex/scripts/standalone_runner.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/configs/apex_btc_mainnet.yaml b/configs/apex_btc_mainnet.yaml index f3e61ec..15974ad 100644 --- a/configs/apex_btc_mainnet.yaml +++ b/configs/apex_btc_mainnet.yaml @@ -6,7 +6,7 @@ allowed_instruments: - BTC-PERP # Entry threshold — the single parameter to tune -radar_score_threshold: 170 +radar_score_threshold: 160 # Scaled for $50 account total_budget: 50.0 diff --git a/skills/apex/scripts/standalone_runner.py b/skills/apex/scripts/standalone_runner.py index 2d8e251..756f1f9 100644 --- a/skills/apex/scripts/standalone_runner.py +++ b/skills/apex/scripts/standalone_runner.py @@ -888,6 +888,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") From 3e6a6b117d9459095dd47ae8b5e1cdb224cfc4f0 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 9 Apr 2026 15:18:30 +0530 Subject: [PATCH 14/17] Reduce min_hold to 15 min so conviction window runs independently min_hold_ms was 45 min, same as conviction_collapse_minutes, causing positions to always close at exactly 45 min regardless of signal state. Now min_hold is 15 min, giving the 45 min conviction window real time to check for signal reappearance before closing. Co-Authored-By: Claude Opus 4.6 --- modules/apex_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/apex_config.py b/modules/apex_config.py index 7814b51..372f735 100644 --- a/modules/apex_config.py +++ b/modules/apex_config.py @@ -28,7 +28,7 @@ class ApexConfig: 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 From ed8683a5abecbc50c3f4e321706d9faf76c6e035 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 9 Apr 2026 20:11:05 +0530 Subject: [PATCH 15/17] Fix weak-peak 45min force-close and stale bullish macro bias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard: extend weak-peak check from 45min to 90min and lower min ROE from 3.0% to 1.5% so normal BTC noise doesn't trigger early exits. Extend Phase 1 hard cap to 180min to match. Radar: add short-term deterioration override to BTC macro. When 4h EMAs say up/strong_up but structural 1h trend is not UP and last 4h price change is negative, downgrade effective macro to neutral. Prevents stale uptrend from boosting longs into a declining market. Log shows override as "BTC strong_up→neutral" when active. Revert raw_score gate in apex_engine — treated symptom not cause. Co-Authored-By: Claude Opus 4.6 --- modules/guard_config.py | 6 +++--- modules/radar_engine.py | 26 +++++++++++++++++++++++--- modules/radar_guard.py | 5 ++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/modules/guard_config.py b/modules/guard_config.py index 1c659d9..0cfdb18 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 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]: From 99e2728f955b0d1d4dc1a2a61146104a1258bad5 Mon Sep 17 00:00:00 2001 From: simp-son Date: Thu, 9 Apr 2026 23:31:33 +0530 Subject: [PATCH 16/17] Bump leverage from 5x to 10x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guard retrace (5%) gives a -50% ROE buffer at 10x — survivable. Position notional doubles to ~$500 on the $50 account. Co-Authored-By: Claude Opus 4.6 --- configs/apex_btc_mainnet.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/apex_btc_mainnet.yaml b/configs/apex_btc_mainnet.yaml index 15974ad..9fc41cd 100644 --- a/configs/apex_btc_mainnet.yaml +++ b/configs/apex_btc_mainnet.yaml @@ -11,7 +11,7 @@ radar_score_threshold: 160 # Scaled for $50 account total_budget: 50.0 max_slots: 1 -leverage: 5.0 +leverage: 10.0 daily_loss_limit: 10.0 guard_preset: tight slot_cooldown_ms: 7200000 # 2 hours — prevents fee churn on re-entry From 2762470f6d2ef658eab5b25ce956e9f6d816c331 Mon Sep 17 00:00:00 2001 From: simp-son Date: Mon, 13 Apr 2026 17:43:50 +0530 Subject: [PATCH 17/17] Fix fee bleed: ALO maker pricing, consecutive loss cooldown, tighter tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ALO entries now price at bid/ask (maker side) instead of mid, preventing automatic fallback to GTC taker. Saves ~$2/round-trip in fees. - Wire consecutive_losses tracking into ApexState + cooldown gate in ApexEngine. After 2 consecutive losses, block entries for 30 min. - Add Tier 0 at 5% ROE (lock 2%) for earlier Phase 2 graduation. Existing tiers tightened: 10%→lock 7%, 20%→lock 15%, 40%→lock 32%. - Bump radar_score_threshold 160→180 to filter low-quality entries. - Port _approve_builder_fee to agent-cli standalone_runner. Co-Authored-By: Claude Opus 4.6 --- cli/hl_adapter.py | 27 ++++++++++------ configs/apex_btc_mainnet.yaml | 2 +- modules/apex_engine.py | 6 +++- modules/apex_state.py | 6 ++++ modules/guard_config.py | 7 ++-- skills/apex/scripts/standalone_runner.py | 41 ++++++++++++++++++++++++ tests/test_trailing_stop.py | 4 +-- 7 files changed, 76 insertions(+), 17 deletions(-) diff --git a/cli/hl_adapter.py b/cli/hl_adapter.py index 620b03a..9fc02a1 100644 --- a/cli/hl_adapter.py +++ b/cli/hl_adapter.py @@ -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) diff --git a/configs/apex_btc_mainnet.yaml b/configs/apex_btc_mainnet.yaml index 9fc41cd..40c937c 100644 --- a/configs/apex_btc_mainnet.yaml +++ b/configs/apex_btc_mainnet.yaml @@ -6,7 +6,7 @@ allowed_instruments: - BTC-PERP # Entry threshold — the single parameter to tune -radar_score_threshold: 160 +radar_score_threshold: 180 # Scaled for $50 account total_budget: 50.0 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 0cfdb18..aacf874 100644 --- a/modules/guard_config.py +++ b/modules/guard_config.py @@ -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/skills/apex/scripts/standalone_runner.py b/skills/apex/scripts/standalone_runner.py index 756f1f9..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: @@ -1013,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/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 # ---------------------------------------------------------------------------