From 5052bc7360588808b34d06cfae1d55d680546e17 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Thu, 2 Jul 2026 08:31:42 -0400 Subject: [PATCH 1/5] chore: archive legacy strategies; keep cfi_hedge funding-rate hedge only Move 18 legacy strategy modules to strategies/_archive and shrink the registry to cfi_hedge only. Update market routing, defaults, docs, and tests while keeping hl hedge and MCP funding_hedge_* tools fully functional. Co-authored-by: Cursor --- README.md | 119 ++++------- cli/commands/run.py | 2 +- cli/config.py | 2 +- cli/mcp_server.py | 2 +- cli/skill.md | 197 ++++++------------ cli/strategy_registry.py | 94 +-------- modules/market_strategy_map.py | 8 +- pyproject.toml | 1 + scripts/entrypoint.py | 2 +- scripts/validate_agent_cli.py | 4 +- strategies/_archive/README.md | 16 ++ strategies/{ => _archive}/aggressive_taker.py | 0 strategies/{ => _archive}/avellaneda_mm.py | 0 strategies/{ => _archive}/basis_arb.py | 0 strategies/{ => _archive}/claude_agent.py | 0 strategies/{ => _archive}/engine_mm.py | 0 strategies/{ => _archive}/funding_arb.py | 0 strategies/{ => _archive}/funding_momentum.py | 0 strategies/{ => _archive}/grid_mm.py | 0 strategies/{ => _archive}/hedge_agent.py | 0 strategies/{ => _archive}/liquidation_mm.py | 0 strategies/{ => _archive}/mean_reversion.py | 0 .../{ => _archive}/momentum_breakout.py | 0 strategies/{ => _archive}/oi_divergence.py | 0 strategies/{ => _archive}/regime_mm.py | 0 strategies/{ => _archive}/rfq_agent.py | 0 strategies/{ => _archive}/risk_multipliers.py | 0 strategies/{ => _archive}/simple_mm.py | 0 .../{ => _archive}/simplified_ensemble.py | 0 strategies/{ => _archive}/trend_follower.py | 0 tests/_archive/README.md | 6 + .../{ => _archive}/test_engine_strategies.py | 0 tests/{ => _archive}/test_new_strategies.py | 0 .../test_strategy_aggressive_taker.py | 0 .../test_strategy_avellaneda.py | 0 .../{ => _archive}/test_strategy_basis_arb.py | 0 .../test_strategy_claude_agent.py | 0 .../{ => _archive}/test_strategy_engine_mm.py | 0 .../{ => _archive}/test_strategy_ensemble.py | 0 .../test_strategy_funding_momentum.py | 0 .../test_strategy_hedge_agent.py | 0 .../test_strategy_mean_reversion.py | 0 .../test_strategy_momentum_breakout.py | 0 .../test_strategy_oi_divergence.py | 0 .../{ => _archive}/test_strategy_qe_wave2.py | 0 tests/{ => _archive}/test_strategy_rfq.py | 0 .../{ => _archive}/test_strategy_simple_mm.py | 0 .../test_strategy_trend_follower.py | 0 tests/test_config.py | 12 +- tests/test_entrypoint.py | 4 +- tests/test_guard_implementations.py | 8 +- tests/test_house_fleet.py | 22 +- tests/test_market_strategy_routing.py | 101 +++------ tests/test_session_policy.py | 2 +- tests/test_strategy_registry.py | 21 +- tests/test_telemetry.py | 2 +- tests/test_trading_surfaces.py | 4 +- 57 files changed, 202 insertions(+), 427 deletions(-) create mode 100644 strategies/_archive/README.md rename strategies/{ => _archive}/aggressive_taker.py (100%) rename strategies/{ => _archive}/avellaneda_mm.py (100%) rename strategies/{ => _archive}/basis_arb.py (100%) rename strategies/{ => _archive}/claude_agent.py (100%) rename strategies/{ => _archive}/engine_mm.py (100%) rename strategies/{ => _archive}/funding_arb.py (100%) rename strategies/{ => _archive}/funding_momentum.py (100%) rename strategies/{ => _archive}/grid_mm.py (100%) rename strategies/{ => _archive}/hedge_agent.py (100%) rename strategies/{ => _archive}/liquidation_mm.py (100%) rename strategies/{ => _archive}/mean_reversion.py (100%) rename strategies/{ => _archive}/momentum_breakout.py (100%) rename strategies/{ => _archive}/oi_divergence.py (100%) rename strategies/{ => _archive}/regime_mm.py (100%) rename strategies/{ => _archive}/rfq_agent.py (100%) rename strategies/{ => _archive}/risk_multipliers.py (100%) rename strategies/{ => _archive}/simple_mm.py (100%) rename strategies/{ => _archive}/simplified_ensemble.py (100%) rename strategies/{ => _archive}/trend_follower.py (100%) create mode 100644 tests/_archive/README.md rename tests/{ => _archive}/test_engine_strategies.py (100%) rename tests/{ => _archive}/test_new_strategies.py (100%) rename tests/{ => _archive}/test_strategy_aggressive_taker.py (100%) rename tests/{ => _archive}/test_strategy_avellaneda.py (100%) rename tests/{ => _archive}/test_strategy_basis_arb.py (100%) rename tests/{ => _archive}/test_strategy_claude_agent.py (100%) rename tests/{ => _archive}/test_strategy_engine_mm.py (100%) rename tests/{ => _archive}/test_strategy_ensemble.py (100%) rename tests/{ => _archive}/test_strategy_funding_momentum.py (100%) rename tests/{ => _archive}/test_strategy_hedge_agent.py (100%) rename tests/{ => _archive}/test_strategy_mean_reversion.py (100%) rename tests/{ => _archive}/test_strategy_momentum_breakout.py (100%) rename tests/{ => _archive}/test_strategy_oi_divergence.py (100%) rename tests/{ => _archive}/test_strategy_qe_wave2.py (100%) rename tests/{ => _archive}/test_strategy_rfq.py (100%) rename tests/{ => _archive}/test_strategy_simple_mm.py (100%) rename tests/{ => _archive}/test_strategy_trend_follower.py (100%) diff --git a/README.md b/README.md index d77b805..4851768 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

Autonomous Trading Agent for Hyperliquid

- 19 strategies • APEX multi-slot orchestrator • REFLECT nightly review • MCP server • Agent Skills + CFI funding-rate hedge • MCP server • Agent Skills

@@ -18,7 +18,7 @@

Python - Strategies + Strategy Tests License MCP @@ -32,7 +32,7 @@ --- -Ship market-making, momentum, arbitrage, and LLM-powered strategies on [Hyperliquid](https://hyperliquid.xyz) perps, Paragon HIP-3 swap markets, and [YEX](https://yex.nunchi.trade) yield perpetuals. Full autonomous stack: Guard trailing stops, Radar opportunity screening, Pulse momentum detection, APEX orchestrator, REFLECT performance review. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. +Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. --- @@ -51,8 +51,9 @@ bash scripts/bootstrap.sh # Creates venv, installs, validates hl wallet auto --save-env # Create wallet + save creds (no prompts) hl setup claim-usdyp # Claim testnet USDyP hl builder approve # Approve builder fee (one-time, testnet) -hl run avellaneda_mm --mock --max-ticks 3 # Validate -hl apex run --mock --max-ticks 5 # Full pipeline test +hl hedge propose BTC --dry-run # Preview CFI v2 hedge proposal +hl hedge status --coin BTC # Active hedges + drift +hl run cfi_hedge --mock --max-ticks 3 # Strategy-loop hedge agent (mock) ``` ### Manual Setup (testnet) @@ -63,7 +64,8 @@ export HL_PRIVATE_KEY=0x... hl setup check hl builder approve -hl run avellaneda_mm -i ETH-PERP --tick 10 +hl hedge propose BTC +hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 3 ``` ### Mainnet @@ -75,7 +77,8 @@ export HL_PRIVATE_KEY=0x... export HL_TESTNET=false hl builder approve --mainnet -hl run avellaneda_mm -i ETH-PERP --tick 10 --mainnet +hl hedge propose BTC +hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 3 --mainnet hl apex run --mainnet ``` @@ -93,10 +96,12 @@ Standard HL perpetuals work on both networks. Examples: `ETH-PERP`, `BTC-PERP`, ```bash # Testnet (default) -hl run avellaneda_mm -i ETH-PERP --tick 10 +hl hedge propose BTC +hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 3 # Mainnet -hl run avellaneda_mm -i BTC-PERP --mainnet --tick 10 +hl hedge propose BTC --mainnet +hl run cfi_hedge -i BTCSWP-PARA --mainnet --mock --max-ticks 3 ``` ### Paragon BTCSWP Swap Perps (HIP-3) @@ -112,12 +117,12 @@ Shorthand `BTCSWP` resolves by network: testnet → `BTCSWP-USDYP` (YEX), mainne ```bash # Paragon swap perp (testnet osrs dex) -hl run avellaneda_mm -i BTCSWP-OSRS --tick 10 +hl hedge propose BTC --dry-run # Paragon swap perp (mainnet) export HL_TESTNET=false hl builder approve --mainnet -hl run engine_mm -i BTCSWP-PARA --mainnet --tick 10 +hl hedge execute BTC --mainnet --dry-run ``` ### YEX Yield Markets (testnet) @@ -131,83 +136,34 @@ hl run engine_mm -i BTCSWP-PARA --mainnet --tick 10 | `BTCSWP-USDYP` | `yex:BTCSWP` | YEX yield perp (distinct from `BTCSWP-OSRS` / `BTCSWP-PARA`) | ```bash -hl run avellaneda_mm -i VXX-USDYP --tick 15 -hl run funding_arb -i US3M-USDYP --tick 30 -hl run engine_mm -i BTCSWP-USDYP --tick 10 # YEX yield BTCSWP, not osrs/para swap +hl hedge backtest --coin BTC --days 30 +hl run cfi_hedge -i BTCSWP-USDYP --mock --max-ticks 3 # YEX yield BTCSWP ``` --- -## Strategies +## Strategy: CFI funding-rate hedge -19 built-in strategies. Every strategy extends `BaseStrategy` with a single `on_tick()` method — no shared state, no hidden coupling. +The product ships **one** registered strategy: **`cfi_hedge`**. Legacy MM/signal strategies were archived on 2026-07-02 under `strategies/_archive/` (see README there). -| Tier | Strategies | Notes | -|------|------------|-------| -| **Quoting engine** | `engine_mm`, `funding_arb`, `regime_mm`, `liquidation_mm` | Require the bundled `quoting_engine` package (composite FV, dynamic spreads, oracle monitor) | -| **Standalone** | `avellaneda_mm`, `simple_mm`, `grid_mm`, `mean_reversion`, `momentum_breakout`, `basis_arb`, `aggressive_taker`, `hedge_agent`, `rfq_agent` | Self-contained; good defaults for testnet and production MM | -| **Experimental / research** | `claude_agent`, `cfi_hedge`, `simplified_ensemble`, `funding_momentum`, `oi_divergence`, `trend_follower` | Less battle-tested; `claude_agent` needs an LLM API key | +| Surface | Command | Purpose | +|---------|---------|---------| +| **Primary** | `hl hedge propose\|execute\|status\|auto\|backtest` | CFI v2 funding-cost hedge on BTCSWP | +| **Strategy loop** | `hl run cfi_hedge -i BTCSWP-PARA` | Same hedge logic inside the standard tick loop | -Run `hl strategies` for the full registry and default parameters. +Math lives in `strategies/cfi_hedge.py` (pure functions). The agent wrapper is `strategies/cfi_hedge_agent.py` (`CfiHedgeAgent`). Opening a hedge sizes a **1/L** CFI v2 leg against an existing perp to neutralize floating funding while leaving the fixed K2 leg exposed. -### Market Making - -Provide two-sided liquidity and earn the spread. These strategies quote bids and asks around a fair value estimate, managing inventory risk through skew and sizing adjustments. - -| Strategy | Description | Key Parameters | When to Use | -|----------|-------------|----------------|-------------| -| `engine_mm` | Production quoting engine — composite 4-signal fair value, dynamic spreads (fee + vol + toxicity + event), inventory skew, multi-level quote ladder. Auto-halts on oracle staleness. *Requires `quoting_engine` module.* | `base_size`, `num_levels` | Primary MM strategy. Handles all market conditions including volatile regimes and stale data. | -| `avellaneda_mm` | Avellaneda-Stoikov optimal market maker. Reservation price adjusts with inventory; optimal spread from risk aversion `gamma` and order flow intensity `k`. Vol-bin classifier + drawdown amplifier. | `gamma`, `k`, `base_size` | When you want theoretically grounded inventory-aware quoting with well-understood parameters. | -| `regime_mm` | Vol-regime adaptive — classifies market into 4 volatility regimes (quiet/normal/volatile/extreme), switches spread width, sizing, and aggressiveness per regime. *Requires `quoting_engine` module.* | `base_size` | Volatile markets where a single spread width doesn't work. Auto-adapts without manual tuning. | -| `simple_mm` | Symmetric bid/ask quoting at fixed spread around mid. No inventory adjustment. | `spread_bps`, `size` | Testnet validation, baseline benchmarking, or low-vol stable pairs. | -| `grid_mm` | Fixed-interval grid levels above and below mid. Places N orders at equal spacing. | `grid_spacing_bps`, `num_levels`, `size_per_level` | Range-bound markets where you want to accumulate and distribute across a price band. | -| `liquidation_mm` | Provides liquidity during cascade/liquidation events. Detects OI drops and widens spreads to capture forced-seller flow. *Requires `quoting_engine` module.* | `oi_drop_threshold_pct`, `cascade_spread_mult` | Liquidation-heavy markets. Only active during cascade conditions — sits idle otherwise. | - -### Arbitrage - -Exploit pricing dislocations across venues, instruments, or time horizons. - -| Strategy | Description | Key Parameters | When to Use | -|----------|-------------|----------------|-------------| -| `funding_arb` | Funding-biased MM on HL funding rate — cross-venue feeds not wired; HL-only bias from funding delta. *Requires `quoting_engine` module.* | `divergence_threshold_bps`, `max_bias_bps` | When HL funding is elevated and you want quote bias toward collecting premium. | -| `basis_arb` | Trades implied basis from funding rate — enters when annualized basis (contango/backwardation) exceeds threshold. | `basis_threshold_bps`, `size` | Capturing contango/backwardation dislocations. Pairs well with funding_arb. | - -### Signal / Directional - -Enter positions based on technical signals or momentum indicators. - -| Strategy | Description | Key Parameters | When to Use | -|----------|-------------|----------------|-------------| -| `momentum_breakout` | Enters on volume + price breakout above/below N-period range. Requires both price and volume confirmation. | `lookback`, `breakout_threshold_bps`, `size` | Trending markets with clear breakout patterns. | -| `mean_reversion` | Trades when price deviates from SMA beyond a threshold. | `window`, `threshold_bps`, `size` | Range-bound markets with predictable mean-reversion behavior. | -| `aggressive_taker` | Crosses the spread with directional bias. Sinusoidal amplitude modulation. | `size`, `bias_amplitude` | When you have strong directional conviction and want immediate fills. | - -### Infrastructure / Risk - -Supporting strategies for portfolio management, block liquidity, and autonomous decision-making. - -| Strategy | Description | Key Parameters | When to Use | -|----------|-------------|----------------|-------------| -| `hedge_agent` | Reduces excess inventory per deterministic mandate. Fires when absolute position qty exceeds threshold. | `inventory_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. | -| `rfq_agent` | Block-size dark RFQ liquidity — quotes for large orders with wider spreads. | `min_size`, `spread_bps` | Institutional/block flow. Provides hidden liquidity for large counterparties. | -| `claude_agent` | Multi-model LLM trading agent. Sends market snapshot to an LLM (Gemini, Claude, or OpenAI), receives structured trade decisions. | `model`, `base_size` | **Experimental.** Autonomous decision-making using LLM reasoning. | - -### Quoting Engine Pipeline - -The engine-powered strategies (`engine_mm`, `funding_arb`, `regime_mm`, `liquidation_mm`) share a common pipeline: - -``` -Market Data -> Composite Fair Value -> Dynamic Spread -> Inventory Skew -> Multi-Level Ladder -> Orders - (4-signal blend) (fee+vol+tox) (price+size) (exponential decay) +```bash +hl hedge propose BTC # Show proposal (no execute) +hl hedge execute BTC --dry-run # Preview order without signing +hl hedge status --coin BTC --watch # Live drift + savings +hl hedge auto --coins BTC --dry-run # Agent-controlled auto-open loop +hl hedge backtest --coin BTC --days 30 # Historical simulation +hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 5 +hl strategies # Registry (cfi_hedge only) ``` -### LLM Agent (Multi-Model) - -| 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` | -| OpenAI | `gpt-4o`, `gpt-4o-mini`, `o3-mini` | `OPENAI_API_KEY` | +MCP tools: `funding_hedge_propose`, `funding_hedge_execute`, `funding_hedge_backtest`, plus `run_strategy` with `strategy=cfi_hedge`. --- @@ -266,7 +222,7 @@ bash scripts/bootstrap.sh # Step 1: Environment hl wallet auto --save-env # Step 2: Wallet hl setup claim-usdyp # Step 4: Fund account hl builder approve # Step 5: Builder fee -hl run avellaneda_mm --mock --max-ticks 3 # Step 6: Validate +hl hedge propose BTC --dry-run # Preview CFI v2 hedge proposal ``` **[Download SKILL.md](skills/onboard/SKILL.md)** @@ -514,7 +470,8 @@ REFLECT auto-generates research directions: ```bash # Core trading -hl run [options] # Start autonomous trading +hl hedge propose|execute|status|auto|backtest # CFI v2 funding hedge (primary) +hl run cfi_hedge [options] # Hedge via strategy loop hl status [--watch] # Show positions, PnL, risk hl trade # Place a single order hl account # Show HL account state @@ -624,7 +581,7 @@ cli/ CLI commands and trading engine builder_fee.py Builder fee config (HL native BuilderInfo) keystore.py Encrypted keystore (geth-compatible) strategy_registry.py Strategy + HIP-3 market definitions (YEX, OSRS, PARA) -strategies/ 19 trading strategy implementations +strategies/ cfi_hedge + archived legacy strategies (_archive/) modules/ Pure logic modules (zero I/O) apex_engine.py APEX decision engine radar_engine.py Opportunity radar diff --git a/cli/commands/run.py b/cli/commands/run.py index dcd5f5e..30cb779 100644 --- a/cli/commands/run.py +++ b/cli/commands/run.py @@ -13,7 +13,7 @@ def run_cmd( strategy: str = typer.Argument( ..., - help="Strategy name (e.g., 'avellaneda_mm') or path ('module:ClassName')", + help="Strategy name (e.g., 'cfi_hedge') or path ('module:ClassName')", ), instrument: str = typer.Option( "ETH-PERP", "--instrument", "-i", diff --git a/cli/config.py b/cli/config.py index 374da24..fedc1dd 100644 --- a/cli/config.py +++ b/cli/config.py @@ -11,7 +11,7 @@ @dataclass class TradingConfig: # Strategy - strategy: str = "avellaneda_mm" + strategy: str = "cfi_hedge" strategy_params: Dict[str, Any] = field(default_factory=dict) # Guard (Dynamic Stop Loss) — optional composable guard diff --git a/cli/mcp_server.py b/cli/mcp_server.py index d000ade..e360fcf 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -590,7 +590,7 @@ def run_strategy( """Start autonomous trading with a strategy. WARNING: places real orders unless dry_run/mock. Args: - strategy: Strategy name (e.g., engine_mm, avellaneda_mm, momentum_breakout) + strategy: Strategy name (cfi_hedge — funding-rate hedge agent) instrument: Trading instrument (default: ETH-PERP) tick: Seconds between ticks (default: 10) max_ticks: Stop after N ticks (None = run forever) diff --git a/cli/skill.md b/cli/skill.md index 7807ca5..4311127 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -1,8 +1,8 @@ --- name: yex-trader -description: Autonomous Hyperliquid trading — 19 strategies (MM, momentum, arbitrage, LLM) with APEX multi-slot orchestrator, REFLECT performance review, Guard trailing stops, and builder fee revenue collection. +description: CFI v2 funding-rate hedge on Hyperliquid BTCSWP — hl hedge propose/execute/status/auto/backtest, MCP funding_hedge_* tools, and cfi_hedge strategy loop. user-invocable: true -argument-hint: " [options]" +argument-hint: "hedge [coin] | run cfi_hedge" allowed-tools: - Bash metadata: @@ -15,9 +15,11 @@ metadata: primaryEnv: HL_PRIVATE_KEY --- -# YEX Trader +# YEX Trader — CFI funding-rate hedge -Autonomous Hyperliquid trading via agent-cli. 19 strategies across market making, momentum, arbitrage, and LLM-powered trading. APEX multi-slot orchestrator. REFLECT nightly performance review. Builder fee revenue collection. +Autonomous Hyperliquid tooling focused on the **CFI v2 funding-cost hedge** (`cfi_hedge`). Legacy MM/signal strategies were archived 2026-07-02 under `strategies/_archive/`. + +Primary surface: **`hl hedge`**. Strategy loop: **`hl run cfi_hedge`**. MCP: `funding_hedge_propose`, `funding_hedge_execute`, `funding_hedge_backtest`. ## Quick Start (Agent-Friendly) @@ -25,10 +27,10 @@ Autonomous Hyperliquid trading via agent-cli. 19 strategies across market making cd ~/agent-cli bash scripts/bootstrap.sh # Creates venv, installs, validates hl wallet auto --save-env # Creates wallet, saves creds to ~/.hl-agent/env -hl setup claim-usdyp # Claim testnet USDyP +hl setup claim-usdyp # Claim testnet USDyP (BTCSWP testnet) hl builder approve # Approve builder fee (one-time) -hl run avellaneda_mm --mock --max-ticks 3 # Validate -hl run engine_mm -i ETH-PERP --tick 15 --max-ticks 5 # First live trade +hl hedge propose BTC --dry-run # Preview hedge proposal +hl run cfi_hedge --mock --max-ticks 3 ``` For full step-by-step onboarding, see `skills/onboard/SKILL.md`. @@ -40,7 +42,7 @@ cd ~/agent-cli && pip install -e . hl setup check # Validate environment ``` -### Getting Started — YEX Testnet +### Getting Started — Testnet 1. Set your private key (or use `hl wallet auto`): ```bash @@ -48,7 +50,7 @@ export HL_PRIVATE_KEY=0x... export HL_TESTNET=true # default ``` -2. Claim testnet USDyP (required for YEX markets): +2. Claim testnet USDyP (required for YEX BTCSWP): ```bash hl setup claim-usdyp ``` @@ -58,31 +60,24 @@ hl setup claim-usdyp hl builder approve ``` -4. Start trading: +4. Hedge workflow: ```bash -hl run avellaneda_mm -i VXX-USDYP --tick 15 # YEX yield market -hl run engine_mm -i ETH-PERP --tick 10 # Standard perp -hl apex run --mock --max-ticks 5 # APEX multi-slot +hl hedge propose BTC # Show proposal +hl hedge execute BTC --dry-run # Preview order (no signing) +hl hedge status --coin BTC # Active hedges +hl hedge auto --coins BTC --dry-run # Auto-open loop +hl run cfi_hedge -i BTCSWP-USDYP --mock --max-ticks 5 ``` -### Getting Started — Mainnet +### Getting Started — Mainnet (para:BTCSWP) -1. Set your private key and network: ```bash export HL_PRIVATE_KEY=0x... export HL_TESTNET=false -``` - -2. Approve builder fee (one-time): -```bash hl builder approve --mainnet -``` - -3. Start trading: -```bash -hl run engine_mm -i ETH-PERP --tick 10 --mainnet # ETH perp -hl run avellaneda_mm -i BTC-PERP --tick 10 --mainnet # BTC perp -hl apex run --mainnet # APEX multi-slot +hl hedge propose BTC --mainnet +hl hedge execute BTC --mainnet --dry-run +hl run cfi_hedge -i BTCSWP-PARA --mainnet --mock --max-ticks 5 ``` ### Environment Variables @@ -94,153 +89,77 @@ hl apex run --mainnet # APEX multi-slot | `HL_TESTNET` | No | `true` (default) or `false` for mainnet | | `BUILDER_ADDRESS` | No | Override builder fee address (default: hardcoded) | | `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) | -| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy | -| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini | \* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required. ## Commands -### Core Trading - -```bash -# Start autonomous trading -hl run [-i INSTRUMENT] [-t TICK] [--config FILE] [--mainnet] [--dry-run] [--mock] [--max-ticks N] - -# Single manual order -hl trade - -# Account info -hl account [--mainnet] - -# Check positions and PnL -hl status [--watch] [--interval 5] - -# List all strategies -hl strategies -``` - -### APEX Multi-Slot Orchestrator - -```bash -hl apex run [-t 60] [--preset conservative|default|aggressive] [--mock] [--budget 1000] [--slots 5] -hl apex once [--mock] -hl apex status -hl apex presets -``` - -### REFLECT Performance Review - -```bash -hl reflect run [--since 2026-03-01] [--data-dir data/cli] -hl reflect report [--date 2026-03-03] -hl reflect history [-n 10] -``` - -### Guard Trailing Stop +### Funding hedge (primary) ```bash -hl guard start ETH-PERP --entry 2500 --size 1 --direction long [--preset tight|moderate] -hl guard status -hl guard presets +hl hedge propose [COIN] [--mainnet] +hl hedge execute [COIN] [--dry-run] [--mainnet] +hl hedge status [--coin C] [--watch] +hl hedge backtest --coin BTC [--days N] +hl hedge auto [--coins ...] [--dry-run] [--mainnet] ``` -### Radar & Movers +### Strategy loop ```bash -hl radar run [--top 10] [--min-score 7.0] -hl radar history [-n 5] -hl movers run [--top 10] +hl run cfi_hedge [-i BTCSWP-PARA|BTCSWP-USDYP|BTCSWP-OSRS] [-t TICK] [--mock] [--dry-run] [--mainnet] +hl strategies # Registry: cfi_hedge only ``` -### Builder Fee +### Supporting operator tools (legacy stack) -```bash -hl builder status -hl builder approve [--mainnet] -``` - -### Wallet (Encrypted Keystore) +APEX, Radar, Pulse, Guard, and REFLECT remain available for local operators but are not part of the default product surface. ```bash -hl wallet auto # Non-interactive wallet creation (agent-friendly) -hl wallet create # Interactive wallet creation -hl wallet import --key -hl wallet list -hl wallet export [--address 0x...] -``` - -### Environment Setup - -```bash -hl setup check # Validate environment -hl setup bootstrap # Auto-create venv and install -hl setup claim-usdyp # Claim testnet USDyP tokens +hl status [--watch] +hl account [--mainnet] +hl trade +hl apex run [--mock] +hl radar once [--mock] +hl guard start ETH-PERP ... +hl reflect run ``` -### MCP Server (~24 Tools) +### MCP Server ```bash -hl mcp serve # Start MCP server (stdio transport) -hl mcp serve --transport sse # Start MCP server (SSE transport) +hl mcp serve ``` -Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context` +Key tools: `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`, `run_strategy` (cfi_hedge), `strategies`, `account`, `status`, `trade`. -## Strategies (19) +## Strategy: cfi_hedge | Name | Type | Description | |------|------|-------------| -| simple_mm | MM | Symmetric bid/ask quoting around mid | -| avellaneda_mm | MM | Inventory-aware Avellaneda-Stoikov model | -| engine_mm | MM | Production quoting engine — composite FV, dynamic spreads, multi-level ladder | -| regime_mm | MM | Vol-regime adaptive — switches behavior by volatility regime (calm/normal/volatile/extreme) | -| grid_mm | MM | Fixed-interval grid levels above and below mid | -| liquidation_mm | MM | Provides liquidity during cascade/liquidation events | -| funding_arb | Arb | HL funding-biased MM (cross-venue feeds not wired) | -| basis_arb | Arb | Trades implied basis from funding rate (contango/backwardation) | -| mean_reversion | Signal | Trades when price deviates from SMA | -| momentum_breakout | Signal | Enters on volume + price breakout above/below N-period range | -| aggressive_taker | Taker | Directional spread crossing with bias | -| hedge_agent | Risk | Reduces excess exposure per deterministic mandate | -| cfi_hedge | Risk | CFI-v2 funding-cost hedge (YEX testnet / Paragon mainnet BTCSWP) | -| rfq_agent | RFQ | Block-size dark RFQ liquidity | -| claude_agent | LLM | Claude/Gemini-powered autonomous trading agent | -| simplified_ensemble | Signal | 6-signal ensemble vote | -| funding_momentum | Signal | Funding rate mean-reversion with EMA confirmation | -| oi_divergence | Signal | Price/OI divergence filter | -| trend_follower | Signal | EMA crossover + ADX trend strength filter | - -## Instruments - -- **Standard perps**: ETH-PERP, BTC-PERP, SOL-PERP, etc. -- **YEX yield markets (testnet)**: VXX-USDYP (`yex:VXX`), US3M-USDYP (`yex:US3M`), BTCSWP-USDYP (`yex:BTCSWP`) -- **Paragon BTCSWP swap perps (HIP-3)**: +| cfi_hedge | Hedge | CFI-v2 funding-cost hedge — opens 1/L CFI v2 leg vs existing perp | + +Archived strategies live in `strategies/_archive/README.md`. + +## Instruments (BTCSWP) | Network | Instrument | HL coin | Notes | |---------|------------|---------|-------| -| Testnet | `BTCSWP-OSRS` | `osrs:BTCSWP` | Paragon swap perp on the `osrs` dex | -| Mainnet | `BTCSWP-PARA` | `para:BTCSWP` | Paragon swap perp on the `para` dex | +| Testnet (YEX) | `BTCSWP-USDYP` | `yex:BTCSWP` | YEX yield BTCSWP | +| Testnet (Paragon) | `BTCSWP-OSRS` | `osrs:BTCSWP` | Paragon swap perp | +| Mainnet | `BTCSWP-PARA` | `para:BTCSWP` | Paragon swap perp | -Shorthand `BTCSWP` resolves by network: testnet → `BTCSWP-USDYP` (YEX yield), mainnet → `BTCSWP-PARA`. Use `BTCSWP-OSRS` or `osrs:BTCSWP` for the explicit Paragon swap perp on testnet. +Shorthand `BTCSWP` resolves by network: testnet → `BTCSWP-USDYP`, mainnet → `BTCSWP-PARA`. ## Workflow 1. **Setup**: `hl setup check` -2. **Claim USDyP** (testnet only): `hl setup claim-usdyp` -3. **Approve builder fee**: `hl builder approve` (testnet) or `hl builder approve --mainnet` -4. **Mock test**: `hl run avellaneda_mm --mock --max-ticks 5` -5. **Dry run**: `hl run engine_mm --dry-run --max-ticks 10` -6. **Live testnet**: `hl run engine_mm -i ETH-PERP --tick 10` -7. **Live mainnet**: `hl run engine_mm -i ETH-PERP --tick 10 --mainnet` -8. **APEX mode**: `hl apex run --mainnet` or `hl apex run --mock --max-ticks 5` -9. **Monitor**: `hl status --watch` -10. **Review**: `hl reflect run` +2. **Claim USDyP** (testnet): `hl setup claim-usdyp` +3. **Approve builder fee**: `hl builder approve` +4. **Preview**: `hl hedge propose BTC --dry-run` +5. **Execute** (after user approval): `hl hedge execute BTC` +6. **Monitor**: `hl hedge status --coin BTC --watch` ## Builder Fee Revenue -Set `BUILDER_ADDRESS` and `BUILDER_FEE_TENTHS_BPS` to collect fees on every trade. Users must approve once via `hl builder approve`. Fee is collected natively by Hyperliquid — no extra gas, no contract calls. - -## REFLECT Self-Improvement - -Run `hl reflect run` after a trading session. REFLECT computes win rate, fee drag ratio (FDR), direction analysis, holding period buckets, monster trade dependency, and generates actionable recommendations. Reports saved to `data/reflect/`. +Set `BUILDER_ADDRESS` and `BUILDER_FEE_TENTHS_BPS` to collect fees on every trade. Users must approve once via `hl builder approve`. diff --git a/cli/strategy_registry.py b/cli/strategy_registry.py index 66eeeed..94f8304 100644 --- a/cli/strategy_registry.py +++ b/cli/strategy_registry.py @@ -4,26 +4,6 @@ from typing import Any, Dict STRATEGY_REGISTRY: Dict[str, Dict[str, Any]] = { - "simple_mm": { - "path": "strategies.simple_mm:SimpleMMStrategy", - "description": "Symmetric bid/ask quoting around mid price", - "params": {"spread_bps": 10.0, "size": 1.0}, - }, - "avellaneda_mm": { - "path": "strategies.avellaneda_mm:AvellanedaStoikovMM", - "description": "Inventory-aware market maker (Avellaneda-Stoikov model)", - "params": {"gamma": 0.1, "k": 1.5, "base_size": 1.0}, - }, - "mean_reversion": { - "path": "strategies.mean_reversion:MeanReversionStrategy", - "description": "Trade when price deviates from SMA", - "params": {"window": 20, "threshold_bps": 30.0, "size": 1.0}, - }, - "hedge_agent": { - "path": "strategies.hedge_agent:HedgeAgent", - "description": "inventory reducer (delta control)", - "params": {"inventory_threshold": 3.0}, - }, "cfi_hedge": { "path": "strategies.cfi_hedge_agent:CfiHedgeAgent", "description": "CFI-v2 funding-cost hedge", @@ -34,76 +14,6 @@ "min_interval_seconds": 300, }, }, - "rfq_agent": { - "path": "strategies.rfq_agent:RFQAgent", - "description": "Block-size liquidity for dark RFQ flow", - "params": {"min_size": 0.5, "spread_bps": 15.0}, - }, - "aggressive_taker": { - "path": "strategies.aggressive_taker:AggressiveTaker", - "description": "Crosses the spread with directional bias", - "params": {"size": 2.0, "bias_amplitude": 0.35}, - }, - "claude_agent": { - "path": "strategies.claude_agent:ClaudeStrategy", - "description": "LLM trading agent — Gemini (default), Claude, OpenAI, or ClawRouter (x402 USDC)", - "params": {"model": "gemini-2.0-flash", "base_size": 0.5}, - }, - "engine_mm": { - "path": "strategies.engine_mm:EngineMMStrategy", - "description": "Production quoting engine MM — composite FV, dynamic spreads, multi-level ladder", - "params": {"base_size": 1.0, "num_levels": 3}, - }, - "funding_arb": { - "path": "strategies.funding_arb:FundingArbStrategy", - "description": "HL funding-rate bias MM — cross-venue arb not wired; HL-only", - "params": {"divergence_threshold_bps": 2.0, "max_bias_bps": 5.0}, - }, - "regime_mm": { - "path": "strategies.regime_mm:RegimeMMStrategy", - "description": "Vol-regime adaptive MM — switches behavior by volatility regime", - "params": {"base_size": 1.0}, - }, - "liquidation_mm": { - "path": "strategies.liquidation_mm:LiquidationMMStrategy", - "description": "Liquidation flow MM — provides liquidity during cascade events", - "params": {"oi_drop_threshold_pct": 5.0, "cascade_spread_mult": 2.5}, - }, - "momentum_breakout": { - "path": "strategies.momentum_breakout:MomentumBreakoutStrategy", - "description": "Momentum breakout — enter on volume + price breakout above/below N-period range", - "params": {"lookback": 20, "breakout_threshold_bps": 50.0, "size": 1.0}, - }, - "grid_mm": { - "path": "strategies.grid_mm:GridMMStrategy", - "description": "Grid market maker — fixed-interval levels above and below mid", - "params": {"grid_spacing_bps": 10.0, "num_levels": 5, "size_per_level": 0.5}, - }, - "basis_arb": { - "path": "strategies.basis_arb:BasisArbStrategy", - "description": "Basis arbitrage — trades implied basis from funding rate", - "params": {"basis_threshold_bps": 5.0, "size": 1.0}, - }, - "simplified_ensemble": { - "path": "strategies.simplified_ensemble:SimplifiedEnsembleStrategy", - "description": "6-signal ensemble (4/6 vote) — ported from auto-research exp52 (score 13.5)", - "params": {"size": 1.0}, - }, - "funding_momentum": { - "path": "strategies.funding_momentum:FundingMomentumStrategy", - "description": "Funding rate mean-reversion — trade extreme funding z-scores with EMA confirmation", - "params": {"size": 1.0}, - }, - "oi_divergence": { - "path": "strategies.oi_divergence:OIDivergenceStrategy", - "description": "OI divergence filter — enter on price/OI agreement, exit on divergence", - "params": {"size": 1.0}, - }, - "trend_follower": { - "path": "strategies.trend_follower:TrendFollowerStrategy", - "description": "EMA crossover + ADX trend strength filter — avoid chop, catch sustained moves", - "params": {"size": 1.0}, - }, } # YEX market definitions — Nunchi HIP-3 yield perpetuals (testnet) @@ -143,8 +53,8 @@ def resolve_strategy_path(name_or_path: str) -> str: """Resolve a short name to a full module:class path. - Accepts either a short name ('avellaneda_mm') or - a full path ('strategies.avellaneda_mm:AvellanedaStoikovMM'). + Accepts either a short name ('cfi_hedge') or + a full path ('strategies.cfi_hedge_agent:CfiHedgeAgent'). """ if ":" in name_or_path: return name_or_path diff --git a/modules/market_strategy_map.py b/modules/market_strategy_map.py index 320d582..e47d23d 100644 --- a/modules/market_strategy_map.py +++ b/modules/market_strategy_map.py @@ -4,11 +4,9 @@ from typing import Dict, List MARKET_STRATEGY_MAP: Dict[str, List[str]] = { - "VXX-USDYP": ["mean_reversion", "simplified_ensemble"], - "BTCSWP-USDYP": ["funding_arb", "funding_momentum", "basis_arb"], - "BTCSWP-OSRS": ["funding_arb", "funding_momentum", "basis_arb"], - "BTCSWP-PARA": ["funding_arb", "funding_momentum", "basis_arb"], - "US3M-USDYP": ["trend_follower", "simplified_ensemble"], + "BTCSWP-USDYP": ["cfi_hedge"], + "BTCSWP-OSRS": ["cfi_hedge"], + "BTCSWP-PARA": ["cfi_hedge"], } diff --git a/pyproject.toml b/pyproject.toml index 0b55b07..f567620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ Repository = "https://github.com/Nunchi-trade/agent-cli" [tool.pytest.ini_options] pythonpath = ["."] +norecursedirs = ["_archive", ".*", "build", "dist"] [tool.mypy] python_version = "3.10" diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index 99fe638..733309a 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -341,7 +341,7 @@ def build_command() -> list[str]: return cmd elif mode == "strategy": - strategy = os.environ.get("STRATEGY", "engine_mm") + strategy = os.environ.get("STRATEGY", "cfi_hedge") instrument = os.environ.get("INSTRUMENT", "ETH-PERP") tick = os.environ.get("TICK_INTERVAL", "10") cmd = py + ["run", strategy, "-i", instrument, "-t", tick] diff --git a/scripts/validate_agent_cli.py b/scripts/validate_agent_cli.py index 5e25af4..b08ca45 100644 --- a/scripts/validate_agent_cli.py +++ b/scripts/validate_agent_cli.py @@ -237,14 +237,14 @@ def e2e_cases(python: str, data_root: Path) -> list[TestCase]: profiles=("e2e", "production"), ), TestCase( - name="run_simple_mm_mock", + name="run_cfi_hedge_mock", stage="e2e_mock", command=python_cmd( [ "-m", "cli.main", "run", - "simple_mm", + "cfi_hedge", "--mock", "--max-ticks", "1", diff --git a/strategies/_archive/README.md b/strategies/_archive/README.md new file mode 100644 index 0000000..63c754a --- /dev/null +++ b/strategies/_archive/README.md @@ -0,0 +1,16 @@ +# Archived legacy strategies + +Deprecated **2026-07-02** as part of the funding-rate hedge product focus. + +The only supported trading strategy is **`cfi_hedge`** (`../cfi_hedge.py`, `../cfi_hedge_agent.py`). +Use `hl hedge propose|execute|status|auto|backtest` for the primary hedge surface. + +These modules are kept for reference and historical tests. They are **not** registered in +`cli/strategy_registry.py` and are not loaded by `hl run` or MCP `run_strategy`. + +## Archived modules + +- simple_mm, avellaneda_mm, mean_reversion, hedge_agent, rfq_agent, aggressive_taker +- claude_agent, engine_mm, funding_arb, regime_mm, liquidation_mm +- momentum_breakout, grid_mm, basis_arb, simplified_ensemble +- funding_momentum, oi_divergence, trend_follower, risk_multipliers diff --git a/strategies/aggressive_taker.py b/strategies/_archive/aggressive_taker.py similarity index 100% rename from strategies/aggressive_taker.py rename to strategies/_archive/aggressive_taker.py diff --git a/strategies/avellaneda_mm.py b/strategies/_archive/avellaneda_mm.py similarity index 100% rename from strategies/avellaneda_mm.py rename to strategies/_archive/avellaneda_mm.py diff --git a/strategies/basis_arb.py b/strategies/_archive/basis_arb.py similarity index 100% rename from strategies/basis_arb.py rename to strategies/_archive/basis_arb.py diff --git a/strategies/claude_agent.py b/strategies/_archive/claude_agent.py similarity index 100% rename from strategies/claude_agent.py rename to strategies/_archive/claude_agent.py diff --git a/strategies/engine_mm.py b/strategies/_archive/engine_mm.py similarity index 100% rename from strategies/engine_mm.py rename to strategies/_archive/engine_mm.py diff --git a/strategies/funding_arb.py b/strategies/_archive/funding_arb.py similarity index 100% rename from strategies/funding_arb.py rename to strategies/_archive/funding_arb.py diff --git a/strategies/funding_momentum.py b/strategies/_archive/funding_momentum.py similarity index 100% rename from strategies/funding_momentum.py rename to strategies/_archive/funding_momentum.py diff --git a/strategies/grid_mm.py b/strategies/_archive/grid_mm.py similarity index 100% rename from strategies/grid_mm.py rename to strategies/_archive/grid_mm.py diff --git a/strategies/hedge_agent.py b/strategies/_archive/hedge_agent.py similarity index 100% rename from strategies/hedge_agent.py rename to strategies/_archive/hedge_agent.py diff --git a/strategies/liquidation_mm.py b/strategies/_archive/liquidation_mm.py similarity index 100% rename from strategies/liquidation_mm.py rename to strategies/_archive/liquidation_mm.py diff --git a/strategies/mean_reversion.py b/strategies/_archive/mean_reversion.py similarity index 100% rename from strategies/mean_reversion.py rename to strategies/_archive/mean_reversion.py diff --git a/strategies/momentum_breakout.py b/strategies/_archive/momentum_breakout.py similarity index 100% rename from strategies/momentum_breakout.py rename to strategies/_archive/momentum_breakout.py diff --git a/strategies/oi_divergence.py b/strategies/_archive/oi_divergence.py similarity index 100% rename from strategies/oi_divergence.py rename to strategies/_archive/oi_divergence.py diff --git a/strategies/regime_mm.py b/strategies/_archive/regime_mm.py similarity index 100% rename from strategies/regime_mm.py rename to strategies/_archive/regime_mm.py diff --git a/strategies/rfq_agent.py b/strategies/_archive/rfq_agent.py similarity index 100% rename from strategies/rfq_agent.py rename to strategies/_archive/rfq_agent.py diff --git a/strategies/risk_multipliers.py b/strategies/_archive/risk_multipliers.py similarity index 100% rename from strategies/risk_multipliers.py rename to strategies/_archive/risk_multipliers.py diff --git a/strategies/simple_mm.py b/strategies/_archive/simple_mm.py similarity index 100% rename from strategies/simple_mm.py rename to strategies/_archive/simple_mm.py diff --git a/strategies/simplified_ensemble.py b/strategies/_archive/simplified_ensemble.py similarity index 100% rename from strategies/simplified_ensemble.py rename to strategies/_archive/simplified_ensemble.py diff --git a/strategies/trend_follower.py b/strategies/_archive/trend_follower.py similarity index 100% rename from strategies/trend_follower.py rename to strategies/_archive/trend_follower.py diff --git a/tests/_archive/README.md b/tests/_archive/README.md new file mode 100644 index 0000000..91a9979 --- /dev/null +++ b/tests/_archive/README.md @@ -0,0 +1,6 @@ +# Archived strategy tests + +Tests for legacy strategies archived on **2026-07-02**. Not collected by default pytest runs +(`norecursedirs` excludes `_archive`). + +Run manually if needed: `pytest tests/_archive/ -v` diff --git a/tests/test_engine_strategies.py b/tests/_archive/test_engine_strategies.py similarity index 100% rename from tests/test_engine_strategies.py rename to tests/_archive/test_engine_strategies.py diff --git a/tests/test_new_strategies.py b/tests/_archive/test_new_strategies.py similarity index 100% rename from tests/test_new_strategies.py rename to tests/_archive/test_new_strategies.py diff --git a/tests/test_strategy_aggressive_taker.py b/tests/_archive/test_strategy_aggressive_taker.py similarity index 100% rename from tests/test_strategy_aggressive_taker.py rename to tests/_archive/test_strategy_aggressive_taker.py diff --git a/tests/test_strategy_avellaneda.py b/tests/_archive/test_strategy_avellaneda.py similarity index 100% rename from tests/test_strategy_avellaneda.py rename to tests/_archive/test_strategy_avellaneda.py diff --git a/tests/test_strategy_basis_arb.py b/tests/_archive/test_strategy_basis_arb.py similarity index 100% rename from tests/test_strategy_basis_arb.py rename to tests/_archive/test_strategy_basis_arb.py diff --git a/tests/test_strategy_claude_agent.py b/tests/_archive/test_strategy_claude_agent.py similarity index 100% rename from tests/test_strategy_claude_agent.py rename to tests/_archive/test_strategy_claude_agent.py diff --git a/tests/test_strategy_engine_mm.py b/tests/_archive/test_strategy_engine_mm.py similarity index 100% rename from tests/test_strategy_engine_mm.py rename to tests/_archive/test_strategy_engine_mm.py diff --git a/tests/test_strategy_ensemble.py b/tests/_archive/test_strategy_ensemble.py similarity index 100% rename from tests/test_strategy_ensemble.py rename to tests/_archive/test_strategy_ensemble.py diff --git a/tests/test_strategy_funding_momentum.py b/tests/_archive/test_strategy_funding_momentum.py similarity index 100% rename from tests/test_strategy_funding_momentum.py rename to tests/_archive/test_strategy_funding_momentum.py diff --git a/tests/test_strategy_hedge_agent.py b/tests/_archive/test_strategy_hedge_agent.py similarity index 100% rename from tests/test_strategy_hedge_agent.py rename to tests/_archive/test_strategy_hedge_agent.py diff --git a/tests/test_strategy_mean_reversion.py b/tests/_archive/test_strategy_mean_reversion.py similarity index 100% rename from tests/test_strategy_mean_reversion.py rename to tests/_archive/test_strategy_mean_reversion.py diff --git a/tests/test_strategy_momentum_breakout.py b/tests/_archive/test_strategy_momentum_breakout.py similarity index 100% rename from tests/test_strategy_momentum_breakout.py rename to tests/_archive/test_strategy_momentum_breakout.py diff --git a/tests/test_strategy_oi_divergence.py b/tests/_archive/test_strategy_oi_divergence.py similarity index 100% rename from tests/test_strategy_oi_divergence.py rename to tests/_archive/test_strategy_oi_divergence.py diff --git a/tests/test_strategy_qe_wave2.py b/tests/_archive/test_strategy_qe_wave2.py similarity index 100% rename from tests/test_strategy_qe_wave2.py rename to tests/_archive/test_strategy_qe_wave2.py diff --git a/tests/test_strategy_rfq.py b/tests/_archive/test_strategy_rfq.py similarity index 100% rename from tests/test_strategy_rfq.py rename to tests/_archive/test_strategy_rfq.py diff --git a/tests/test_strategy_simple_mm.py b/tests/_archive/test_strategy_simple_mm.py similarity index 100% rename from tests/test_strategy_simple_mm.py rename to tests/_archive/test_strategy_simple_mm.py diff --git a/tests/test_strategy_trend_follower.py b/tests/_archive/test_strategy_trend_follower.py similarity index 100% rename from tests/test_strategy_trend_follower.py rename to tests/_archive/test_strategy_trend_follower.py diff --git a/tests/test_config.py b/tests/test_config.py index 53d019d..c2f8cdd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,7 +11,7 @@ class TestDefaults: def test_default_values(self): cfg = TradingConfig() - assert cfg.strategy == "avellaneda_mm" + assert cfg.strategy == "cfi_hedge" assert cfg.instrument == "ETH-PERP" assert cfg.mainnet is False assert cfg.dry_run is False @@ -29,11 +29,11 @@ def test_custom_risk_not_default(self): class TestFromYaml: def test_loads_valid_yaml(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - f.write("strategy: engine_mm\ninstrument: BTC-PERP\ntick_interval: 30.0\n") + f.write("strategy: cfi_hedge\ninstrument: BTC-PERP\ntick_interval: 30.0\n") f.flush() cfg = TradingConfig.from_yaml(f.name) os.unlink(f.name) - assert cfg.strategy == "engine_mm" + assert cfg.strategy == "cfi_hedge" assert cfg.instrument == "BTC-PERP" assert cfg.tick_interval == 30.0 @@ -43,15 +43,15 @@ def test_empty_yaml(self): f.flush() cfg = TradingConfig.from_yaml(f.name) os.unlink(f.name) - assert cfg.strategy == "avellaneda_mm" # defaults + assert cfg.strategy == "cfi_hedge" # defaults def test_unknown_fields_ignored(self): with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - f.write("strategy: simple_mm\nunknown_field: 42\n") + f.write("strategy: cfi_hedge\nunknown_field: 42\n") f.flush() cfg = TradingConfig.from_yaml(f.name) os.unlink(f.name) - assert cfg.strategy == "simple_mm" + assert cfg.strategy == "cfi_hedge" assert not hasattr(cfg, "unknown_field") def test_missing_file_raises(self): diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index c89dfee..1456700 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -80,14 +80,14 @@ def test_wolf_mode(self, monkeypatch): def test_strategy_mode(self, monkeypatch): monkeypatch.setenv("RUN_MODE", "strategy") - monkeypatch.setenv("STRATEGY", "engine_mm") + monkeypatch.setenv("STRATEGY", "cfi_hedge") monkeypatch.setenv("INSTRUMENT", "BTC-PERP") monkeypatch.setenv("TICK_INTERVAL", "5") monkeypatch.setenv("HL_TESTNET", "true") cmd = build_command() assert "run" in cmd - assert "engine_mm" in cmd + assert "cfi_hedge" in cmd assert "-i" in cmd assert "BTC-PERP" in cmd assert "-t" in cmd diff --git a/tests/test_guard_implementations.py b/tests/test_guard_implementations.py index 0905d49..afdda92 100644 --- a/tests/test_guard_implementations.py +++ b/tests/test_guard_implementations.py @@ -152,16 +152,16 @@ def test_init_disabled(self): assert guard.enabled is False def test_loads_valid_strategy(self): - guard = StrategyGuard(strategy_names=["simple_mm"], enabled=True) + guard = StrategyGuard(strategy_names=["cfi_hedge"], enabled=True) assert len(guard.strategies) == 1 def test_skips_invalid_strategy(self): guard = StrategyGuard(strategy_names=["nonexistent_xyz"], enabled=True) assert len(guard.strategies) == 0 - def test_multiple_strategies(self): + def test_multiple_strategies_not_supported(self): guard = StrategyGuard( - strategy_names=["simple_mm", "mean_reversion"], + strategy_names=["cfi_hedge"], enabled=True, ) - assert len(guard.strategies) == 2 + assert len(guard.strategies) == 1 diff --git a/tests/test_house_fleet.py b/tests/test_house_fleet.py index bf73371..735e3f6 100644 --- a/tests/test_house_fleet.py +++ b/tests/test_house_fleet.py @@ -175,13 +175,13 @@ def test_resolve_member_wallet_env_reference(self, monkeypatch): def test_build_env_does_not_inherit_parent_wallet(self, monkeypatch): monkeypatch.setenv("HL_PRIVATE_KEY", "0xparent") sup = FleetSupervisor() - spec = FleetMemberSpec(name="g", strategy="engine_mm") + spec = FleetMemberSpec(name="g", strategy="cfi_hedge") assert "HL_PRIVATE_KEY" not in sup._build_env(spec) def test_build_env_sets_explicit_member_wallet(self, monkeypatch): monkeypatch.setenv("HL_PRIVATE_KEY", "0xparent") sup = FleetSupervisor() - spec = FleetMemberSpec(name="g", strategy="engine_mm", wallet="0xmember") + spec = FleetMemberSpec(name="g", strategy="cfi_hedge", wallet="0xmember") env = sup._build_env(spec) assert env["HL_PRIVATE_KEY"] == "0xmember" @@ -191,7 +191,7 @@ def test_build_env_sets_member_wallet_from_env_reference(self, monkeypatch): sup = FleetSupervisor() spec = FleetMemberSpec( name="g", - strategy="engine_mm", + strategy="cfi_hedge", wallet="env:MEMBER2_HL_PRIVATE_KEY", ) env = sup._build_env(spec) @@ -199,16 +199,16 @@ def test_build_env_sets_member_wallet_from_env_reference(self, monkeypatch): def test_build_args_run(self): sup = FleetSupervisor() - spec = FleetMemberSpec(name="g", strategy="engine_mm", market="xyz:GOLD", + spec = FleetMemberSpec(name="g", strategy="cfi_hedge", market="xyz:GOLD", extra_args=["--mock", "--max-ticks", "30"]) assert sup._build_args(spec) == [ - "run", "engine_mm", "-i", "xyz:GOLD", "--mock", "--max-ticks", "30" + "run", "cfi_hedge", "-i", "xyz:GOLD", "--mock", "--max-ticks", "30" ] def test_build_args_no_market(self): sup = FleetSupervisor() - spec = FleetMemberSpec(name="m", strategy="engine_mm") - assert sup._build_args(spec) == ["run", "engine_mm"] + spec = FleetMemberSpec(name="m", strategy="cfi_hedge") + assert sup._build_args(spec) == ["run", "cfi_hedge"] def test_build_args_load_sentinel(self): """__load__ members emit `strategy load ` (sibling-PR subcommand).""" @@ -222,13 +222,13 @@ class TestFleetSupervisorLifecycle: def test_spawn_mock_member_exits_cleanly(self): """Spawn a real self-terminating member and assert state transitions. - Uses `run engine_mm --mock --max-ticks 2` with a tiny tick so it runs + Uses `run cfi_hedge --mock --max-ticks 2` with a tiny tick so it runs offline (no HL connection) and exits on its own. """ sup = FleetSupervisor() spec = FleetMemberSpec( name="mock-gold", - strategy="engine_mm", + strategy="cfi_hedge", market="ETH-PERP", extra_args=["--mock", "--max-ticks", "2", "--tick", "0.05", "--fresh"], ) @@ -259,7 +259,7 @@ def test_kill_marks_killed(self): sup = FleetSupervisor() spec = FleetMemberSpec( name="long", - strategy="engine_mm", + strategy="cfi_hedge", market="ETH-PERP", # max-ticks 0 => runs forever; large tick so it idles between ticks. extra_args=["--mock", "--max-ticks", "0", "--tick", "30", "--fresh"], @@ -282,7 +282,7 @@ def test_missing_preset_soft_fails(self): sup = FleetSupervisor() spec = FleetMemberSpec( name="p", - strategy="engine_mm", + strategy="cfi_hedge", market="ETH-PERP", preset="does-not-exist-xyz", extra_args=["--mock", "--max-ticks", "1", "--tick", "0.05", "--fresh"], diff --git a/tests/test_market_strategy_routing.py b/tests/test_market_strategy_routing.py index f8067e7..16ee22a 100644 --- a/tests/test_market_strategy_routing.py +++ b/tests/test_market_strategy_routing.py @@ -11,41 +11,23 @@ class TestMarketStrategyMap: - def test_vxx_mapping(self): - strats = get_strategies_for_market("VXX-USDYP") - assert "mean_reversion" in strats - assert "simplified_ensemble" in strats - def test_btcswp_osrs_mapping(self): - strats = get_strategies_for_market("BTCSWP-OSRS") - assert "funding_arb" in strats - assert "funding_momentum" in strats - assert "basis_arb" in strats + assert get_strategies_for_market("BTCSWP-OSRS") == ["cfi_hedge"] def test_btcswp_para_mapping(self): - strats = get_strategies_for_market("BTCSWP-PARA") - assert "funding_arb" in strats - assert "funding_momentum" in strats - assert "basis_arb" in strats + assert get_strategies_for_market("BTCSWP-PARA") == ["cfi_hedge"] def test_btcswp_mapping(self): - strats = get_strategies_for_market("BTCSWP-USDYP") - assert "funding_arb" in strats - assert "funding_momentum" in strats - assert "basis_arb" in strats - - def test_us3m_mapping(self): - strats = get_strategies_for_market("US3M-USDYP") - assert "trend_follower" in strats - assert "simplified_ensemble" in strats + assert get_strategies_for_market("BTCSWP-USDYP") == ["cfi_hedge"] def test_unmapped_market_returns_empty(self): assert get_strategies_for_market("ETH-PERP") == [] assert get_strategies_for_market("BTC-PERP") == [] + assert get_strategies_for_market("VXX-USDYP") == [] assert get_strategies_for_market("UNKNOWN") == [] def test_has_strategy_mapping_true(self): - assert has_strategy_mapping(["VXX-USDYP"]) is True + assert has_strategy_mapping(["BTCSWP-PARA"]) is True assert has_strategy_mapping(["ETH-PERP", "BTCSWP-USDYP"]) is True def test_has_strategy_mapping_false(self): @@ -60,7 +42,7 @@ class TestStrategyGuardRouting: def _make_all_markets(coins: dict) -> list: """Build a minimal all_markets structure for given coins. - coins: {"VXX": 30.5, "BTC": 95000, ...} + coins: {"BTCSWP": 95000, ...} """ universe = [{"name": coin} for coin in coins] ctxs = [ @@ -76,38 +58,28 @@ def _make_all_markets(coins: dict) -> list: return [{"universe": universe}, ctxs] def test_routed_scan_only_runs_mapped_strategies(self): - """When target_markets is set, only mapped strategies run per market.""" guard = StrategyGuard( - target_markets=["VXX-USDYP"], + target_markets=["BTCSWP-PARA"], enabled=True, ) - # VXX should map to mean_reversion and simplified_ensemble - assert "mean_reversion" in [ - name for name in MARKET_STRATEGY_MAP["VXX-USDYP"] - ] - # The guard should have loaded strategies on-demand (cache starts empty) - assert len(guard.strategies) == 0 # no legacy strategies loaded - assert len(guard._strategy_cache) == 0 # cache empty until scan() + assert MARKET_STRATEGY_MAP["BTCSWP-PARA"] == ["cfi_hedge"] + assert len(guard.strategies) == 0 + assert len(guard._strategy_cache) == 0 def test_routed_scan_with_market_data(self): - """Routed scan should produce signals for mapped markets.""" guard = StrategyGuard( - target_markets=["VXX-USDYP"], + target_markets=["BTCSWP-PARA"], enabled=True, ) - all_markets = self._make_all_markets({"VXX": 30.5}) - signals = guard.scan(all_markets=all_markets, target_markets=["VXX-USDYP"]) + all_markets = self._make_all_markets({"BTCSWP": 95000}) + signals = guard.scan(all_markets=all_markets, target_markets=["BTCSWP-PARA"]) - # Strategies were loaded into cache assert len(guard._strategy_cache) > 0 - - # Signals (if any) should reference VXX, not random coins for sig in signals: - assert sig["asset"] == "VXX" + assert sig["asset"] == "BTCSWP" assert "strategy:" in sig["source"] def test_routed_scan_skips_unmapped_markets(self): - """Markets without a mapping should produce no strategy signals.""" guard = StrategyGuard( target_markets=["ETH-PERP"], enabled=True, @@ -117,41 +89,37 @@ def test_routed_scan_skips_unmapped_markets(self): assert signals == [] def test_legacy_scan_still_works(self): - """Without target_markets, legacy all×all behavior is preserved.""" guard = StrategyGuard( - strategy_names=["simple_mm"], + strategy_names=["cfi_hedge"], enabled=True, ) - all_markets = self._make_all_markets({"ETH": 3500}) - # Legacy scan — no target_markets + all_markets = self._make_all_markets({"BTC": 95000}) signals = guard.scan(all_markets=all_markets) - # simple_mm should produce signals (bid/ask quotes) - assert len(signals) > 0 + assert isinstance(signals, list) def test_disabled_guard_returns_empty(self): guard = StrategyGuard( - target_markets=["VXX-USDYP"], + target_markets=["BTCSWP-PARA"], enabled=False, ) - all_markets = self._make_all_markets({"VXX": 30.5}) + all_markets = self._make_all_markets({"BTCSWP": 95000}) signals = guard.scan(all_markets=all_markets) assert signals == [] def test_find_snapshot_by_coin_prefix(self): - """_find_snapshot should match 'VXX-USDYP' to a snapshot keyed 'VXX'.""" snapshots = { - "VXX": MarketSnapshot( - instrument="VXX-PERP", - mid_price=30.5, - bid=30.49, - ask=30.51, - spread_bps=6.5, + "BTCSWP": MarketSnapshot( + instrument="BTCSWP-PARA", + mid_price=95000.0, + bid=94999.0, + ask=95001.0, + spread_bps=0.2, timestamp_ms=int(time.time() * 1000), ), } - snap = StrategyGuard._find_snapshot(snapshots, "VXX-USDYP") + snap = StrategyGuard._find_snapshot(snapshots, "BTCSWP-PARA") assert snap is not None - assert snap.mid_price == 30.5 + assert snap.mid_price == 95000.0 def test_find_snapshot_no_match(self): snapshots = { @@ -164,15 +132,14 @@ def test_find_snapshot_no_match(self): timestamp_ms=int(time.time() * 1000), ), } - snap = StrategyGuard._find_snapshot(snapshots, "VXX-USDYP") + snap = StrategyGuard._find_snapshot(snapshots, "BTCSWP-PARA") assert snap is None def test_strategy_cache_reuse(self): - """Strategies should be loaded once and cached.""" - guard = StrategyGuard(target_markets=["VXX-USDYP"], enabled=True) - s1 = guard._get_or_load("mean_reversion") - s2 = guard._get_or_load("mean_reversion") - assert s1 is s2 # same instance + guard = StrategyGuard(target_markets=["BTCSWP-PARA"], enabled=True) + s1 = guard._get_or_load("cfi_hedge") + s2 = guard._get_or_load("cfi_hedge") + assert s1 is s2 class TestApexConfigAllowedInstruments: @@ -183,5 +150,5 @@ def test_allowed_instruments_default_empty(self): def test_allowed_instruments_from_dict(self): from modules.apex_config import ApexConfig - cfg = ApexConfig.from_dict({"allowed_instruments": ["VXX-USDYP", "BTCSWP-USDYP"]}) - assert cfg.allowed_instruments == ["VXX-USDYP", "BTCSWP-USDYP"] + cfg = ApexConfig.from_dict({"allowed_instruments": ["BTCSWP-PARA"]}) + assert cfg.allowed_instruments == ["BTCSWP-PARA"] diff --git a/tests/test_session_policy.py b/tests/test_session_policy.py index 00008ad..b1f90b6 100644 --- a/tests/test_session_policy.py +++ b/tests/test_session_policy.py @@ -362,7 +362,7 @@ def test_run_wallet_allowlist_refuses_wrong_signer_before_loop(self, monkeypatch self._app(), [ "run", - "engine_mm", + "cfi_hedge", "--mock", "--max-ticks", "1", diff --git a/tests/test_strategy_registry.py b/tests/test_strategy_registry.py index 565ec3d..1aeade9 100644 --- a/tests/test_strategy_registry.py +++ b/tests/test_strategy_registry.py @@ -11,8 +11,8 @@ class TestResolveStrategyPath: def test_valid_short_name(self): - path = resolve_strategy_path("simple_mm") - assert path == "strategies.simple_mm:SimpleMMStrategy" + path = resolve_strategy_path("cfi_hedge") + assert path == "strategies.cfi_hedge_agent:CfiHedgeAgent" def test_all_registered_strategies_resolve(self): for name in STRATEGY_REGISTRY: @@ -28,20 +28,23 @@ def test_invalid_name_raises(self): resolve_strategy_path("nonexistent_strategy") def test_error_shows_available(self): - with pytest.raises(ValueError, match="simple_mm"): + with pytest.raises(ValueError, match="cfi_hedge"): resolve_strategy_path("bad_name") - def test_claude_agent_registered(self): - path = resolve_strategy_path("claude_agent") - assert "ClaudeStrategy" in path + def test_cfi_hedge_registered(self): + path = resolve_strategy_path("cfi_hedge") + assert "CfiHedgeAgent" in path def test_registry_has_params(self): for name, entry in STRATEGY_REGISTRY.items(): assert "path" in entry assert "description" in entry - def test_hedge_agent_param_matches_class(self): - assert STRATEGY_REGISTRY["hedge_agent"]["params"] == {"inventory_threshold": 3.0} + def test_only_cfi_hedge_registered(self): + assert set(STRATEGY_REGISTRY.keys()) == {"cfi_hedge"} + + def test_cfi_hedge_param_defaults(self): + assert STRATEGY_REGISTRY["cfi_hedge"]["params"]["notional_trigger"] == 100000.0 class TestResolveInstrument: @@ -59,8 +62,6 @@ def test_yex_coin_reverse_lookup(self): def test_yex_btcswp_reverse_lookup(self): assert resolve_instrument("yex:BTCSWP") == "BTCSWP-USDYP" - - def test_osrs_btcswp_reverse_lookup(self): assert resolve_instrument("osrs:BTCSWP", mainnet=False) == "BTCSWP-OSRS" assert resolve_instrument("BTCSWP-OSRS", mainnet=False) == "BTCSWP-OSRS" diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 07c90b4..feff42a 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -30,7 +30,7 @@ def test_case_insensitive_address(self): def test_different_strategy_different_id(self): c1 = _make_client(strategy_name="apex") - c2 = _make_client(strategy_name="simple_mm") + c2 = _make_client(strategy_name="cfi_hedge") assert c1.instance_id != c2.instance_id def test_16_char_hex(self): diff --git a/tests/test_trading_surfaces.py b/tests/test_trading_surfaces.py index 2d940e5..cc800be 100644 --- a/tests/test_trading_surfaces.py +++ b/tests/test_trading_surfaces.py @@ -100,7 +100,7 @@ def mark_init(self, *args, **kwargs): monkeypatch.setenv("HL_VIEW_AS_USER", self.ADDR) monkeypatch.setattr(cfgmod.TradingConfig, "__init__", mark_init) - result = runner.invoke(app, ["run", "engine_mm", "--mock", "--max-ticks", "1"]) + result = runner.invoke(app, ["run", "cfi_hedge", "--mock", "--max-ticks", "1"]) self._assert_refused(result) assert called is False @@ -269,7 +269,7 @@ def test_summary_with_synthetic_trades(self, tmp_data_dir): assert data["pnl"]["round_trips"] == 1 assert data["pnl"]["win_rate"] == 100.0 # registry is always present - assert "avellaneda_mm" in data["registry"]["strategies"] + assert "cfi_hedge" in data["registry"]["strategies"] assert data["view_only"] is False def test_summary_empty_dir_zeros(self, tmp_data_dir): From 4d8232f4fe8125e33a9e967c4331d100d337b58a Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Thu, 2 Jul 2026 16:03:24 -0400 Subject: [PATCH 2/5] chore: archive quoting_engine, SEDA oracle, legacy deploy templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move quoting_engine (incl. seda_oracle), Hermes/OpenClaw Railway deploy templates, and QE tests to archive/. Hedge uses strategies/cfi_funding.py (HL fundingHistory only) — no SEDA dependency. Co-authored-by: Cursor --- README.md | 10 +++-- archive/README.md | 22 +++++++++++ .../deploy}/hermes-railway/Dockerfile | 0 .../deploy}/hermes-railway/package.json | 0 .../deploy}/hermes-railway/railway.toml | 4 +- .../deploy}/hermes-railway/src/bootstrap.mjs | 0 .../deploy}/hermes-railway/src/gateway.js | 0 .../deploy}/hermes-railway/src/onboard.js | 0 .../deploy}/hermes-railway/src/server.js | 0 .../deploy}/hermes-railway/src/status.js | 0 .../hermes-railway/workspace/AGENTS.md | 0 .../hermes-railway/workspace/BOOTSTRAP.md | 0 .../deploy}/hermes-railway/workspace/SOUL.md | 0 .../deploy}/hermes-railway/workspace/TOOLS.md | 0 .../deploy}/openclaw-railway/.env.example | 0 .../deploy}/openclaw-railway/Dockerfile | 0 .../deploy}/openclaw-railway/package.json | 0 .../deploy}/openclaw-railway/railway.toml | 4 +- .../openclaw-railway/src/bootstrap.mjs | 0 .../deploy}/openclaw-railway/src/gateway.js | 0 .../deploy}/openclaw-railway/src/onboard.js | 0 .../deploy}/openclaw-railway/src/server.js | 0 .../deploy}/openclaw-railway/src/status.js | 0 .../openclaw-railway/workspace/AGENTS.md | 0 .../openclaw-railway/workspace/BOOTSTRAP.md | 0 .../openclaw-railway/workspace/SOUL.md | 0 .../openclaw-railway/workspace/TOOLS.md | 0 .../quoting_engine}/__init__.py | 0 .../quoting_engine}/config.py | 0 .../configs/events/default_calendar.yaml | 0 .../quoting_engine}/configs/funding_rate.yaml | 0 .../quoting_engine}/configs/us3m.yaml | 0 .../quoting_engine}/configs/vxxn.yaml | 0 .../quoting_engine}/engine.py | 0 .../quoting_engine}/event_schedule.py | 0 .../quoting_engine}/fair_value.py | 0 .../quoting_engine}/feeds/__init__.py | 0 .../quoting_engine}/feeds/base.py | 0 .../quoting_engine}/feeds/funding_rate.py | 0 .../quoting_engine}/feeds/microprice.py | 0 .../quoting_engine}/feeds/oracle_monitor.py | 0 .../quoting_engine}/feeds/seda_oracle.py | 0 .../quoting_engine}/inventory.py | 0 .../quoting_engine}/ladder.py | 0 .../quoting_engine}/metrics.py | 0 .../quoting_engine}/spread.py | 0 .../quoting_engine}/toxicity.py | 0 .../quoting_engine}/vol_estimator.py | 0 cli/api/status_reader.py | 2 +- cli/commands/hedge.py | 16 ++++---- cli/hedge_display.py | 2 +- docs/hl_feature_audit.md | 2 +- pyproject.toml | 4 +- scripts/validate_agent_cli.py | 1 - strategies/cfi_funding.py | 38 +++++++++++++++++++ tests/_archive/README.md | 9 ++--- .../{ => _archive}/quoting_engine/conftest.py | 4 ++ .../quoting_engine/test_qe_config.py | 0 .../quoting_engine/test_qe_disagreement.py | 0 .../quoting_engine/test_qe_engine.py | 0 .../test_qe_event_schedule_calendar.py | 0 .../quoting_engine/test_qe_fair_value.py | 0 .../quoting_engine/test_qe_feeds_base.py | 0 .../test_qe_funding_boundary.py | 0 .../test_qe_funding_rate_feed.py | 0 .../quoting_engine/test_qe_fv_band.py | 0 .../quoting_engine/test_qe_inventory.py | 0 .../quoting_engine/test_qe_inventory_caps.py | 0 .../quoting_engine/test_qe_ladder.py | 0 .../quoting_engine/test_qe_liq_advanced.py | 0 .../quoting_engine/test_qe_metrics.py | 0 .../quoting_engine/test_qe_microprice.py | 0 .../quoting_engine/test_qe_oracle_monitor.py | 0 .../quoting_engine/test_qe_regime.py | 0 .../quoting_engine/test_qe_spread.py | 0 .../quoting_engine/test_qe_toxicity.py | 0 .../quoting_engine/test_qe_toxicity_tiered.py | 0 .../quoting_engine/test_qe_vol_estimator.py | 0 78 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 archive/README.md rename {deploy => archive/deploy}/hermes-railway/Dockerfile (100%) rename {deploy => archive/deploy}/hermes-railway/package.json (100%) rename {deploy => archive/deploy}/hermes-railway/railway.toml (78%) rename {deploy => archive/deploy}/hermes-railway/src/bootstrap.mjs (100%) rename {deploy => archive/deploy}/hermes-railway/src/gateway.js (100%) rename {deploy => archive/deploy}/hermes-railway/src/onboard.js (100%) rename {deploy => archive/deploy}/hermes-railway/src/server.js (100%) rename {deploy => archive/deploy}/hermes-railway/src/status.js (100%) rename {deploy => archive/deploy}/hermes-railway/workspace/AGENTS.md (100%) rename {deploy => archive/deploy}/hermes-railway/workspace/BOOTSTRAP.md (100%) rename {deploy => archive/deploy}/hermes-railway/workspace/SOUL.md (100%) rename {deploy => archive/deploy}/hermes-railway/workspace/TOOLS.md (100%) rename {deploy => archive/deploy}/openclaw-railway/.env.example (100%) rename {deploy => archive/deploy}/openclaw-railway/Dockerfile (100%) rename {deploy => archive/deploy}/openclaw-railway/package.json (100%) rename {deploy => archive/deploy}/openclaw-railway/railway.toml (79%) rename {deploy => archive/deploy}/openclaw-railway/src/bootstrap.mjs (100%) rename {deploy => archive/deploy}/openclaw-railway/src/gateway.js (100%) rename {deploy => archive/deploy}/openclaw-railway/src/onboard.js (100%) rename {deploy => archive/deploy}/openclaw-railway/src/server.js (100%) rename {deploy => archive/deploy}/openclaw-railway/src/status.js (100%) rename {deploy => archive/deploy}/openclaw-railway/workspace/AGENTS.md (100%) rename {deploy => archive/deploy}/openclaw-railway/workspace/BOOTSTRAP.md (100%) rename {deploy => archive/deploy}/openclaw-railway/workspace/SOUL.md (100%) rename {deploy => archive/deploy}/openclaw-railway/workspace/TOOLS.md (100%) rename {quoting_engine => archive/quoting_engine}/__init__.py (100%) rename {quoting_engine => archive/quoting_engine}/config.py (100%) rename {quoting_engine => archive/quoting_engine}/configs/events/default_calendar.yaml (100%) rename {quoting_engine => archive/quoting_engine}/configs/funding_rate.yaml (100%) rename {quoting_engine => archive/quoting_engine}/configs/us3m.yaml (100%) rename {quoting_engine => archive/quoting_engine}/configs/vxxn.yaml (100%) rename {quoting_engine => archive/quoting_engine}/engine.py (100%) rename {quoting_engine => archive/quoting_engine}/event_schedule.py (100%) rename {quoting_engine => archive/quoting_engine}/fair_value.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/__init__.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/base.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/funding_rate.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/microprice.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/oracle_monitor.py (100%) rename {quoting_engine => archive/quoting_engine}/feeds/seda_oracle.py (100%) rename {quoting_engine => archive/quoting_engine}/inventory.py (100%) rename {quoting_engine => archive/quoting_engine}/ladder.py (100%) rename {quoting_engine => archive/quoting_engine}/metrics.py (100%) rename {quoting_engine => archive/quoting_engine}/spread.py (100%) rename {quoting_engine => archive/quoting_engine}/toxicity.py (100%) rename {quoting_engine => archive/quoting_engine}/vol_estimator.py (100%) create mode 100644 strategies/cfi_funding.py rename tests/{ => _archive}/quoting_engine/conftest.py (83%) rename tests/{ => _archive}/quoting_engine/test_qe_config.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_disagreement.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_engine.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_event_schedule_calendar.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_fair_value.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_feeds_base.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_funding_boundary.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_funding_rate_feed.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_fv_band.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_inventory.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_inventory_caps.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_ladder.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_liq_advanced.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_metrics.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_microprice.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_oracle_monitor.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_regime.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_spread.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_toxicity.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_toxicity_tiered.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_vol_estimator.py (100%) diff --git a/README.md b/README.md index 4851768..4cb91f9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,9 @@ --- -Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. +Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. K2 inputs come from HL funding history only (no SEDA). Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. + +Legacy market-making (`quoting_engine`), SEDA oracle, and Hermes/OpenClaw Railway deploy templates were archived 2026-07-02 under `archive/` — see `archive/README.md`. --- @@ -524,7 +526,7 @@ mcp_servers: # cwd: /path/to/agent-cli # if not launched from the repo root ``` -This is exactly what `deploy/hermes-railway` auto-generates on boot (alongside `platform_toolsets`), so a deployed Hermes agent is wired out of the box — no OpenClaw-specific assumptions anywhere in the MCP server. +This is exactly what the legacy `archive/deploy/hermes-railway` template auto-generates on boot (alongside `platform_toolsets`), so a deployed Hermes agent is wired out of the box — no OpenClaw-specific assumptions anywhere in the MCP server. ### HTTP API & SSE @@ -565,8 +567,8 @@ self-host templates. They are not part of the new hosted MCP subscription architecture because they provision user-facing conversational/autonomous agents. Do not expose them as the paid Nunchi product path. -For legacy experiments, use `deploy/openclaw-railway/railway.toml` or -`deploy/hermes-railway/railway.toml` as the config-as-code path and keep the +For legacy experiments, use `archive/deploy/openclaw-railway/railway.toml` or +`archive/deploy/hermes-railway/railway.toml` as the config-as-code path and keep the Railway build root at the repo root. --- diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 0000000..7cd75ff --- /dev/null +++ b/archive/README.md @@ -0,0 +1,22 @@ +# Archived legacy components + +**Date:** 2026-07-02 +**Reason:** Product focus narrowed to CFI v2 funding-rate hedge only (`hl hedge`, `cfi_hedge` strategy). Market-making stack, SEDA oracle, and legacy agent deploy templates are no longer maintained in the active tree. + +## What moved here + +| Path | Description | +|------|-------------| +| `archive/quoting_engine/` | Full market-making stack (fair value, spreads, ladders, toxicity, funding feeds). Includes `feeds/seda_oracle.py` (deprecated). | +| `archive/deploy/hermes-railway/` | Hermes agent gateway Railway template | +| `archive/deploy/openclaw-railway/` | OpenClaw agent gateway Railway template | + +## Active replacement + +Hedge K2 inputs now come from **Hyperliquid funding only** via `strategies/cfi_funding.py` — no SEDA oracle. + +## Running archived tests manually + +```bash +PYTHONPATH=archive:. pytest tests/_archive/quoting_engine/ -v +``` diff --git a/deploy/hermes-railway/Dockerfile b/archive/deploy/hermes-railway/Dockerfile similarity index 100% rename from deploy/hermes-railway/Dockerfile rename to archive/deploy/hermes-railway/Dockerfile diff --git a/deploy/hermes-railway/package.json b/archive/deploy/hermes-railway/package.json similarity index 100% rename from deploy/hermes-railway/package.json rename to archive/deploy/hermes-railway/package.json diff --git a/deploy/hermes-railway/railway.toml b/archive/deploy/hermes-railway/railway.toml similarity index 78% rename from deploy/hermes-railway/railway.toml rename to archive/deploy/hermes-railway/railway.toml index a1db35e..16afa37 100644 --- a/deploy/hermes-railway/railway.toml +++ b/archive/deploy/hermes-railway/railway.toml @@ -2,8 +2,8 @@ builder = "dockerfile" # Build context is the repo root so the Dockerfile can COPY the agent-cli source. # Set this service's Root Directory to the repo root (/) in Railway, not -# deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "deploy/hermes-railway/Dockerfile" +# archive/deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package. +dockerfilePath = "archive/deploy/hermes-railway/Dockerfile" [deploy] healthcheckPath = "/health" diff --git a/deploy/hermes-railway/src/bootstrap.mjs b/archive/deploy/hermes-railway/src/bootstrap.mjs similarity index 100% rename from deploy/hermes-railway/src/bootstrap.mjs rename to archive/deploy/hermes-railway/src/bootstrap.mjs diff --git a/deploy/hermes-railway/src/gateway.js b/archive/deploy/hermes-railway/src/gateway.js similarity index 100% rename from deploy/hermes-railway/src/gateway.js rename to archive/deploy/hermes-railway/src/gateway.js diff --git a/deploy/hermes-railway/src/onboard.js b/archive/deploy/hermes-railway/src/onboard.js similarity index 100% rename from deploy/hermes-railway/src/onboard.js rename to archive/deploy/hermes-railway/src/onboard.js diff --git a/deploy/hermes-railway/src/server.js b/archive/deploy/hermes-railway/src/server.js similarity index 100% rename from deploy/hermes-railway/src/server.js rename to archive/deploy/hermes-railway/src/server.js diff --git a/deploy/hermes-railway/src/status.js b/archive/deploy/hermes-railway/src/status.js similarity index 100% rename from deploy/hermes-railway/src/status.js rename to archive/deploy/hermes-railway/src/status.js diff --git a/deploy/hermes-railway/workspace/AGENTS.md b/archive/deploy/hermes-railway/workspace/AGENTS.md similarity index 100% rename from deploy/hermes-railway/workspace/AGENTS.md rename to archive/deploy/hermes-railway/workspace/AGENTS.md diff --git a/deploy/hermes-railway/workspace/BOOTSTRAP.md b/archive/deploy/hermes-railway/workspace/BOOTSTRAP.md similarity index 100% rename from deploy/hermes-railway/workspace/BOOTSTRAP.md rename to archive/deploy/hermes-railway/workspace/BOOTSTRAP.md diff --git a/deploy/hermes-railway/workspace/SOUL.md b/archive/deploy/hermes-railway/workspace/SOUL.md similarity index 100% rename from deploy/hermes-railway/workspace/SOUL.md rename to archive/deploy/hermes-railway/workspace/SOUL.md diff --git a/deploy/hermes-railway/workspace/TOOLS.md b/archive/deploy/hermes-railway/workspace/TOOLS.md similarity index 100% rename from deploy/hermes-railway/workspace/TOOLS.md rename to archive/deploy/hermes-railway/workspace/TOOLS.md diff --git a/deploy/openclaw-railway/.env.example b/archive/deploy/openclaw-railway/.env.example similarity index 100% rename from deploy/openclaw-railway/.env.example rename to archive/deploy/openclaw-railway/.env.example diff --git a/deploy/openclaw-railway/Dockerfile b/archive/deploy/openclaw-railway/Dockerfile similarity index 100% rename from deploy/openclaw-railway/Dockerfile rename to archive/deploy/openclaw-railway/Dockerfile diff --git a/deploy/openclaw-railway/package.json b/archive/deploy/openclaw-railway/package.json similarity index 100% rename from deploy/openclaw-railway/package.json rename to archive/deploy/openclaw-railway/package.json diff --git a/deploy/openclaw-railway/railway.toml b/archive/deploy/openclaw-railway/railway.toml similarity index 79% rename from deploy/openclaw-railway/railway.toml rename to archive/deploy/openclaw-railway/railway.toml index bd0e575..50d7479 100644 --- a/deploy/openclaw-railway/railway.toml +++ b/archive/deploy/openclaw-railway/railway.toml @@ -2,8 +2,8 @@ builder = "dockerfile" # Build context is the repo root so the Dockerfile can COPY the agent-cli source. # Set this service's Root Directory to the repo root (/) in Railway, not -# deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "deploy/openclaw-railway/Dockerfile" +# archive/deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package. +dockerfilePath = "archive/deploy/openclaw-railway/Dockerfile" [deploy] healthcheckPath = "/health" diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/archive/deploy/openclaw-railway/src/bootstrap.mjs similarity index 100% rename from deploy/openclaw-railway/src/bootstrap.mjs rename to archive/deploy/openclaw-railway/src/bootstrap.mjs diff --git a/deploy/openclaw-railway/src/gateway.js b/archive/deploy/openclaw-railway/src/gateway.js similarity index 100% rename from deploy/openclaw-railway/src/gateway.js rename to archive/deploy/openclaw-railway/src/gateway.js diff --git a/deploy/openclaw-railway/src/onboard.js b/archive/deploy/openclaw-railway/src/onboard.js similarity index 100% rename from deploy/openclaw-railway/src/onboard.js rename to archive/deploy/openclaw-railway/src/onboard.js diff --git a/deploy/openclaw-railway/src/server.js b/archive/deploy/openclaw-railway/src/server.js similarity index 100% rename from deploy/openclaw-railway/src/server.js rename to archive/deploy/openclaw-railway/src/server.js diff --git a/deploy/openclaw-railway/src/status.js b/archive/deploy/openclaw-railway/src/status.js similarity index 100% rename from deploy/openclaw-railway/src/status.js rename to archive/deploy/openclaw-railway/src/status.js diff --git a/deploy/openclaw-railway/workspace/AGENTS.md b/archive/deploy/openclaw-railway/workspace/AGENTS.md similarity index 100% rename from deploy/openclaw-railway/workspace/AGENTS.md rename to archive/deploy/openclaw-railway/workspace/AGENTS.md diff --git a/deploy/openclaw-railway/workspace/BOOTSTRAP.md b/archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md similarity index 100% rename from deploy/openclaw-railway/workspace/BOOTSTRAP.md rename to archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md diff --git a/deploy/openclaw-railway/workspace/SOUL.md b/archive/deploy/openclaw-railway/workspace/SOUL.md similarity index 100% rename from deploy/openclaw-railway/workspace/SOUL.md rename to archive/deploy/openclaw-railway/workspace/SOUL.md diff --git a/deploy/openclaw-railway/workspace/TOOLS.md b/archive/deploy/openclaw-railway/workspace/TOOLS.md similarity index 100% rename from deploy/openclaw-railway/workspace/TOOLS.md rename to archive/deploy/openclaw-railway/workspace/TOOLS.md diff --git a/quoting_engine/__init__.py b/archive/quoting_engine/__init__.py similarity index 100% rename from quoting_engine/__init__.py rename to archive/quoting_engine/__init__.py diff --git a/quoting_engine/config.py b/archive/quoting_engine/config.py similarity index 100% rename from quoting_engine/config.py rename to archive/quoting_engine/config.py diff --git a/quoting_engine/configs/events/default_calendar.yaml b/archive/quoting_engine/configs/events/default_calendar.yaml similarity index 100% rename from quoting_engine/configs/events/default_calendar.yaml rename to archive/quoting_engine/configs/events/default_calendar.yaml diff --git a/quoting_engine/configs/funding_rate.yaml b/archive/quoting_engine/configs/funding_rate.yaml similarity index 100% rename from quoting_engine/configs/funding_rate.yaml rename to archive/quoting_engine/configs/funding_rate.yaml diff --git a/quoting_engine/configs/us3m.yaml b/archive/quoting_engine/configs/us3m.yaml similarity index 100% rename from quoting_engine/configs/us3m.yaml rename to archive/quoting_engine/configs/us3m.yaml diff --git a/quoting_engine/configs/vxxn.yaml b/archive/quoting_engine/configs/vxxn.yaml similarity index 100% rename from quoting_engine/configs/vxxn.yaml rename to archive/quoting_engine/configs/vxxn.yaml diff --git a/quoting_engine/engine.py b/archive/quoting_engine/engine.py similarity index 100% rename from quoting_engine/engine.py rename to archive/quoting_engine/engine.py diff --git a/quoting_engine/event_schedule.py b/archive/quoting_engine/event_schedule.py similarity index 100% rename from quoting_engine/event_schedule.py rename to archive/quoting_engine/event_schedule.py diff --git a/quoting_engine/fair_value.py b/archive/quoting_engine/fair_value.py similarity index 100% rename from quoting_engine/fair_value.py rename to archive/quoting_engine/fair_value.py diff --git a/quoting_engine/feeds/__init__.py b/archive/quoting_engine/feeds/__init__.py similarity index 100% rename from quoting_engine/feeds/__init__.py rename to archive/quoting_engine/feeds/__init__.py diff --git a/quoting_engine/feeds/base.py b/archive/quoting_engine/feeds/base.py similarity index 100% rename from quoting_engine/feeds/base.py rename to archive/quoting_engine/feeds/base.py diff --git a/quoting_engine/feeds/funding_rate.py b/archive/quoting_engine/feeds/funding_rate.py similarity index 100% rename from quoting_engine/feeds/funding_rate.py rename to archive/quoting_engine/feeds/funding_rate.py diff --git a/quoting_engine/feeds/microprice.py b/archive/quoting_engine/feeds/microprice.py similarity index 100% rename from quoting_engine/feeds/microprice.py rename to archive/quoting_engine/feeds/microprice.py diff --git a/quoting_engine/feeds/oracle_monitor.py b/archive/quoting_engine/feeds/oracle_monitor.py similarity index 100% rename from quoting_engine/feeds/oracle_monitor.py rename to archive/quoting_engine/feeds/oracle_monitor.py diff --git a/quoting_engine/feeds/seda_oracle.py b/archive/quoting_engine/feeds/seda_oracle.py similarity index 100% rename from quoting_engine/feeds/seda_oracle.py rename to archive/quoting_engine/feeds/seda_oracle.py diff --git a/quoting_engine/inventory.py b/archive/quoting_engine/inventory.py similarity index 100% rename from quoting_engine/inventory.py rename to archive/quoting_engine/inventory.py diff --git a/quoting_engine/ladder.py b/archive/quoting_engine/ladder.py similarity index 100% rename from quoting_engine/ladder.py rename to archive/quoting_engine/ladder.py diff --git a/quoting_engine/metrics.py b/archive/quoting_engine/metrics.py similarity index 100% rename from quoting_engine/metrics.py rename to archive/quoting_engine/metrics.py diff --git a/quoting_engine/spread.py b/archive/quoting_engine/spread.py similarity index 100% rename from quoting_engine/spread.py rename to archive/quoting_engine/spread.py diff --git a/quoting_engine/toxicity.py b/archive/quoting_engine/toxicity.py similarity index 100% rename from quoting_engine/toxicity.py rename to archive/quoting_engine/toxicity.py diff --git a/quoting_engine/vol_estimator.py b/archive/quoting_engine/vol_estimator.py similarity index 100% rename from quoting_engine/vol_estimator.py rename to archive/quoting_engine/vol_estimator.py diff --git a/cli/api/status_reader.py b/cli/api/status_reader.py index 1cda7ce..fa017dc 100644 --- a/cli/api/status_reader.py +++ b/cli/api/status_reader.py @@ -2,7 +2,7 @@ Shared utility used by: - scripts/entrypoint.py (imported directly) -- deploy/openclaw-railway/src/server.js (via `python3 -m cli.api.status_reader`) +- archive/deploy/openclaw-railway/src/server.js (via `python3 -m cli.api.status_reader`) """ from __future__ import annotations diff --git a/cli/commands/hedge.py b/cli/commands/hedge.py index ffcfa63..9f83dd7 100644 --- a/cli/commands/hedge.py +++ b/cli/commands/hedge.py @@ -8,8 +8,8 @@ hl hedge backtest --coin BTC [--days N] wrap hedge_calculator.py hl hedge auto [--coins ...] [--dry-run] agent-controlled auto-open loop -All math + oracle access is delegated to `strategies/cfi_hedge.py` and -`quoting_engine/feeds/seda_oracle.py`. Signing + submission are delegated to +All math + funding inputs are delegated to `strategies/cfi_hedge.py` and +`strategies/cfi_funding.py` (HL funding rates only). Signing + submission are delegated to the Hyperliquid Python SDK via `DirectHLProxy.place_order()` — same code path as `hl trade`. No custom signing here. """ @@ -116,8 +116,8 @@ def _build_proposal( Returns (proposal, snapshot) or raises typer.Exit if no position open. """ from strategies.cfi_hedge import build_cfi_hedge_proposal, get_cfi_profile - from quoting_engine.feeds.seda_oracle import ( - fetch_btcswp_snapshot, + from strategies.cfi_funding import ( + fetch_cfi_funding_snapshot, fetch_hl_current_funding_hr, ) @@ -145,7 +145,7 @@ def _build_proposal( raise typer.Exit(1) position = _position_to_summary(raw_pos, coin_override=coin) - snapshot = fetch_btcswp_snapshot(profile) + snapshot = fetch_cfi_funding_snapshot(profile) current_funding = fetch_hl_current_funding_hr(coin) if current_funding is None: # Fall back to oracle's r_ema if HL didn't answer. @@ -303,8 +303,8 @@ def status_cmd( get_cfi_profile, ) from cli.hedge_display import hedge_status_block - from quoting_engine.feeds.seda_oracle import ( - fetch_btcswp_snapshot, + from strategies.cfi_funding import ( + fetch_cfi_funding_snapshot, fetch_hl_current_funding_hr, ) @@ -327,7 +327,7 @@ def _refresh(): live.append({"job": h, "snapshot": None, "drift_apy": 0.0, "savings_usd": h.get("cumulative_savings_usd", 0.0)}) continue try: - snap = fetch_btcswp_snapshot(profile) + snap = fetch_cfi_funding_snapshot(profile) except Exception: snap = None current_hr = fetch_hl_current_funding_hr(h.get("coin", "BTC")) diff --git a/cli/hedge_display.py b/cli/hedge_display.py index 8db8e33..5e9e267 100644 --- a/cli/hedge_display.py +++ b/cli/hedge_display.py @@ -101,7 +101,7 @@ def hedge_proposal_block(proposal, snapshot, *, mainnet: bool = False) -> str: else "↓" if proposal.wire_drift_per_hour_usd < -0.5 else "↔" ) - src = "SEDA live" if snapshot.source == "seda" else f"{YELLOW}replay (HL fundingHistory){RESET}" + src = f"{GREEN}HL fundingHistory{RESET}" network = "mainnet" if mainnet else "testnet" lines = [ diff --git a/docs/hl_feature_audit.md b/docs/hl_feature_audit.md index 44f3bab..f0f1692 100644 --- a/docs/hl_feature_audit.md +++ b/docs/hl_feature_audit.md @@ -172,7 +172,7 @@ These read `snapshot.funding_rate` in their trading logic but don't import any H **Tightly coupled (quoting engine + HyperliquidFundingRate, 4 strategies):** `engine_mm`, `funding_arb`, `regime_mm`, `liquidation_mm` -These directly import `HyperliquidFundingRate` from the quoting engine and require `_engine_base.py` (path hack to `~/Tee-work-/quoting_engine`). Porting requires refactoring the quoting engine's funding rate interface. +These directly import `HyperliquidFundingRate` from the quoting engine (now under `archive/quoting_engine/`) and require `_engine_base.py` (path hack to `~/Tee-work-/quoting_engine`). Porting requires refactoring the quoting engine's funding rate interface. ### Highest-Priority Abstractions for Multi-Exchange Support diff --git a/pyproject.toml b/pyproject.toml index f567620..97a45fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ Repository = "https://github.com/Nunchi-trade/agent-cli" [tool.pytest.ini_options] pythonpath = ["."] -norecursedirs = ["_archive", ".*", "build", "dist"] +norecursedirs = ["_archive", "archive", ".*", "build", "dist"] [tool.mypy] python_version = "3.10" @@ -56,4 +56,4 @@ disallow_untyped_defs = false ignore_missing_imports = true [tool.setuptools.packages.find] -include = ["cli*", "strategies*", "sdk*", "common*", "parent*", "modules*", "skills*", "quoting_engine*"] +include = ["cli*", "strategies*", "sdk*", "common*", "parent*", "modules*", "skills*"] diff --git a/scripts/validate_agent_cli.py b/scripts/validate_agent_cli.py index b08ca45..72821bc 100644 --- a/scripts/validate_agent_cli.py +++ b/scripts/validate_agent_cli.py @@ -130,7 +130,6 @@ def base_cases(python: str, quick_pytest: bool) -> list[TestCase]: "strategies", "modules", "skills", - "quoting_engine", "scripts", ], python, diff --git a/strategies/cfi_funding.py b/strategies/cfi_funding.py new file mode 100644 index 0000000..ffd5f48 --- /dev/null +++ b/strategies/cfi_funding.py @@ -0,0 +1,38 @@ +"""CFI v2 funding inputs from Hyperliquid only.""" +from __future__ import annotations +import logging, os, time +from dataclasses import dataclass +from typing import Literal, Optional +import requests +from strategies.cfi_hedge import BTCSWP_PROFILE, CFIAssetProfile, FundingRateSample, compute_k2_from_history +log = logging.getLogger(__name__) +HL_INFO_URL_DEFAULT = "https://api.hyperliquid.xyz/info" +def _hl_info_url() -> str: return os.environ.get("HL_INFO_URL", HL_INFO_URL_DEFAULT) +@dataclass(frozen=True) +class CfiFundingSnapshot: + source: Literal["hl"]; timestamp_iso: str; timestamp_ms: int; oracle_px: Optional[float] + k_fixed_hr: float; r_ema_hr: float; cfi: Optional[float] +def _fetch_funding_history(coin, lookback_hours, *, timeout_s=10.0): + end_time = int(time.time()*1000); start_time = end_time - lookback_hours*3600000 + resp = requests.post(_hl_info_url(), json={"type":"fundingHistory","coin":coin,"startTime":start_time,"endTime":end_time}, timeout=timeout_s) + resp.raise_for_status(); return resp.json() or [] +def fetch_hl_current_funding_hr(coin, *, timeout_s=10.0): + try: + resp = requests.post(_hl_info_url(), json={"type":"metaAndAssetCtxs"}, timeout=timeout_s); resp.raise_for_status(); data = resp.json() + for meta, ctx in zip(data[0]["universe"], data[1]): + if meta.get("name")==coin: return float(ctx.get("funding",0)) + return None + except Exception as e: + log.warning("cfi_funding: fetch_hl_current_funding_hr(%s) failed: %s", coin, e); return None +def replay_k2_from_hl(profile=BTCSWP_PROFILE, *, lookback_hours=168, timeout_s=10.0): + hist = _fetch_funding_history(profile.hl_coin, lookback_hours, timeout_s=timeout_s) + samples = [FundingRateSample(funding_rate=float(e["fundingRate"]), time=int(e["time"])) for e in hist] + k = compute_k2_from_history(samples, profile) + if samples: + r_ema = samples[0].funding_rate + for s in samples: r_ema = (1-profile.k2_beta)*r_ema + profile.k2_beta*s.funding_rate + else: r_ema = profile.fixed_leg_initial + now_ms = int(time.time()*1000) + return CfiFundingSnapshot(source="hl", timestamp_iso=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms/1000)), timestamp_ms=now_ms, oracle_px=None, k_fixed_hr=k, r_ema_hr=r_ema, cfi=None) +def fetch_cfi_funding_snapshot(profile=BTCSWP_PROFILE, *, timeout_s=10.0): return replay_k2_from_hl(profile, timeout_s=timeout_s) +BTCSWPOracleSnapshot = CfiFundingSnapshot; fetch_btcswp_snapshot = fetch_cfi_funding_snapshot diff --git a/tests/_archive/README.md b/tests/_archive/README.md index 91a9979..a2eea21 100644 --- a/tests/_archive/README.md +++ b/tests/_archive/README.md @@ -1,6 +1,5 @@ -# Archived strategy tests +# Archived strategy and quoting-engine tests -Tests for legacy strategies archived on **2026-07-02**. Not collected by default pytest runs -(`norecursedirs` excludes `_archive`). - -Run manually if needed: `pytest tests/_archive/ -v` +```bash +PYTHONPATH=archive:. pytest tests/_archive/quoting_engine/ -v +``` diff --git a/tests/quoting_engine/conftest.py b/tests/_archive/quoting_engine/conftest.py similarity index 83% rename from tests/quoting_engine/conftest.py rename to tests/_archive/quoting_engine/conftest.py index 4339da0..c0deb60 100644 --- a/tests/quoting_engine/conftest.py +++ b/tests/_archive/quoting_engine/conftest.py @@ -1,5 +1,9 @@ """Shared fixtures for quoting engine tests.""" +import sys +from pathlib import Path import pytest +_archive_root = Path(__file__).resolve().parents[2] / "archive" +if str(_archive_root) not in sys.path: sys.path.insert(0, str(_archive_root)) from quoting_engine.config import ( MarketConfig, FairValueWeights, SpreadParams, LadderParams, SkewParams, diff --git a/tests/quoting_engine/test_qe_config.py b/tests/_archive/quoting_engine/test_qe_config.py similarity index 100% rename from tests/quoting_engine/test_qe_config.py rename to tests/_archive/quoting_engine/test_qe_config.py diff --git a/tests/quoting_engine/test_qe_disagreement.py b/tests/_archive/quoting_engine/test_qe_disagreement.py similarity index 100% rename from tests/quoting_engine/test_qe_disagreement.py rename to tests/_archive/quoting_engine/test_qe_disagreement.py diff --git a/tests/quoting_engine/test_qe_engine.py b/tests/_archive/quoting_engine/test_qe_engine.py similarity index 100% rename from tests/quoting_engine/test_qe_engine.py rename to tests/_archive/quoting_engine/test_qe_engine.py diff --git a/tests/quoting_engine/test_qe_event_schedule_calendar.py b/tests/_archive/quoting_engine/test_qe_event_schedule_calendar.py similarity index 100% rename from tests/quoting_engine/test_qe_event_schedule_calendar.py rename to tests/_archive/quoting_engine/test_qe_event_schedule_calendar.py diff --git a/tests/quoting_engine/test_qe_fair_value.py b/tests/_archive/quoting_engine/test_qe_fair_value.py similarity index 100% rename from tests/quoting_engine/test_qe_fair_value.py rename to tests/_archive/quoting_engine/test_qe_fair_value.py diff --git a/tests/quoting_engine/test_qe_feeds_base.py b/tests/_archive/quoting_engine/test_qe_feeds_base.py similarity index 100% rename from tests/quoting_engine/test_qe_feeds_base.py rename to tests/_archive/quoting_engine/test_qe_feeds_base.py diff --git a/tests/quoting_engine/test_qe_funding_boundary.py b/tests/_archive/quoting_engine/test_qe_funding_boundary.py similarity index 100% rename from tests/quoting_engine/test_qe_funding_boundary.py rename to tests/_archive/quoting_engine/test_qe_funding_boundary.py diff --git a/tests/quoting_engine/test_qe_funding_rate_feed.py b/tests/_archive/quoting_engine/test_qe_funding_rate_feed.py similarity index 100% rename from tests/quoting_engine/test_qe_funding_rate_feed.py rename to tests/_archive/quoting_engine/test_qe_funding_rate_feed.py diff --git a/tests/quoting_engine/test_qe_fv_band.py b/tests/_archive/quoting_engine/test_qe_fv_band.py similarity index 100% rename from tests/quoting_engine/test_qe_fv_band.py rename to tests/_archive/quoting_engine/test_qe_fv_band.py diff --git a/tests/quoting_engine/test_qe_inventory.py b/tests/_archive/quoting_engine/test_qe_inventory.py similarity index 100% rename from tests/quoting_engine/test_qe_inventory.py rename to tests/_archive/quoting_engine/test_qe_inventory.py diff --git a/tests/quoting_engine/test_qe_inventory_caps.py b/tests/_archive/quoting_engine/test_qe_inventory_caps.py similarity index 100% rename from tests/quoting_engine/test_qe_inventory_caps.py rename to tests/_archive/quoting_engine/test_qe_inventory_caps.py diff --git a/tests/quoting_engine/test_qe_ladder.py b/tests/_archive/quoting_engine/test_qe_ladder.py similarity index 100% rename from tests/quoting_engine/test_qe_ladder.py rename to tests/_archive/quoting_engine/test_qe_ladder.py diff --git a/tests/quoting_engine/test_qe_liq_advanced.py b/tests/_archive/quoting_engine/test_qe_liq_advanced.py similarity index 100% rename from tests/quoting_engine/test_qe_liq_advanced.py rename to tests/_archive/quoting_engine/test_qe_liq_advanced.py diff --git a/tests/quoting_engine/test_qe_metrics.py b/tests/_archive/quoting_engine/test_qe_metrics.py similarity index 100% rename from tests/quoting_engine/test_qe_metrics.py rename to tests/_archive/quoting_engine/test_qe_metrics.py diff --git a/tests/quoting_engine/test_qe_microprice.py b/tests/_archive/quoting_engine/test_qe_microprice.py similarity index 100% rename from tests/quoting_engine/test_qe_microprice.py rename to tests/_archive/quoting_engine/test_qe_microprice.py diff --git a/tests/quoting_engine/test_qe_oracle_monitor.py b/tests/_archive/quoting_engine/test_qe_oracle_monitor.py similarity index 100% rename from tests/quoting_engine/test_qe_oracle_monitor.py rename to tests/_archive/quoting_engine/test_qe_oracle_monitor.py diff --git a/tests/quoting_engine/test_qe_regime.py b/tests/_archive/quoting_engine/test_qe_regime.py similarity index 100% rename from tests/quoting_engine/test_qe_regime.py rename to tests/_archive/quoting_engine/test_qe_regime.py diff --git a/tests/quoting_engine/test_qe_spread.py b/tests/_archive/quoting_engine/test_qe_spread.py similarity index 100% rename from tests/quoting_engine/test_qe_spread.py rename to tests/_archive/quoting_engine/test_qe_spread.py diff --git a/tests/quoting_engine/test_qe_toxicity.py b/tests/_archive/quoting_engine/test_qe_toxicity.py similarity index 100% rename from tests/quoting_engine/test_qe_toxicity.py rename to tests/_archive/quoting_engine/test_qe_toxicity.py diff --git a/tests/quoting_engine/test_qe_toxicity_tiered.py b/tests/_archive/quoting_engine/test_qe_toxicity_tiered.py similarity index 100% rename from tests/quoting_engine/test_qe_toxicity_tiered.py rename to tests/_archive/quoting_engine/test_qe_toxicity_tiered.py diff --git a/tests/quoting_engine/test_qe_vol_estimator.py b/tests/_archive/quoting_engine/test_qe_vol_estimator.py similarity index 100% rename from tests/quoting_engine/test_qe_vol_estimator.py rename to tests/_archive/quoting_engine/test_qe_vol_estimator.py From 5d90420d5d8c5047d119b74b7f32466884bfb6bf Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Fri, 3 Jul 2026 13:26:25 -0400 Subject: [PATCH 3/5] chore: archive APEX stack; remove SEDA from hedge path (#72) * chore: archive APEX stack and remove SEDA from hedge path Move APEX/Radar/Pulse/Guard/Reflect, quoting_engine, adapters, deploy templates, and related tests to _archive/. Hedge K2 inputs now come from Hyperliquid funding history via strategies/cfi_funding.py with no SEDA oracle. Co-authored-by: Cursor * docs: mark archived operator stack as legacy Co-authored-by: Cursor --------- Co-authored-by: Cursor --- README.md | 303 ++---------------- _archive/README.md | 34 ++ {adapters => _archive/adapters}/__init__.py | 0 {adapters => _archive/adapters}/hl_adapter.py | 0 .../adapters}/mock_adapter.py | 0 {cli => _archive/cli}/commands/apex.py | 0 {cli => _archive/cli}/commands/guard.py | 0 {cli => _archive/cli}/commands/pulse.py | 0 {cli => _archive/cli}/commands/radar.py | 0 {cli => _archive/cli}/commands/reflect.py | 0 {configs => _archive/configs}/yex_btcswp.yaml | 0 .../configs}/yex_us3m_protected.yaml | 0 .../configs}/yex_vxx_protected.yaml | 0 .../deploy}/hermes-railway/Dockerfile | 0 .../deploy}/hermes-railway/package.json | 0 .../deploy}/hermes-railway/railway.toml | 4 +- .../deploy}/hermes-railway/src/bootstrap.mjs | 0 .../deploy}/hermes-railway/src/gateway.js | 0 .../deploy}/hermes-railway/src/onboard.js | 0 .../deploy}/hermes-railway/src/server.js | 0 .../deploy}/hermes-railway/src/status.js | 0 .../hermes-railway/workspace/AGENTS.md | 0 .../hermes-railway/workspace/BOOTSTRAP.md | 0 .../deploy}/hermes-railway/workspace/SOUL.md | 0 .../deploy}/hermes-railway/workspace/TOOLS.md | 0 .../deploy}/openclaw-railway/.env.example | 0 .../deploy}/openclaw-railway/Dockerfile | 0 .../deploy}/openclaw-railway/package.json | 0 .../deploy}/openclaw-railway/railway.toml | 4 +- .../openclaw-railway/src/bootstrap.mjs | 0 .../deploy}/openclaw-railway/src/gateway.js | 0 .../deploy}/openclaw-railway/src/onboard.js | 0 .../deploy}/openclaw-railway/src/server.js | 0 .../deploy}/openclaw-railway/src/status.js | 0 .../openclaw-railway/workspace/AGENTS.md | 0 .../openclaw-railway/workspace/BOOTSTRAP.md | 0 .../openclaw-railway/workspace/SOUL.md | 0 .../openclaw-railway/workspace/TOOLS.md | 0 {docs => _archive/docs}/hl_feature_audit.md | 2 +- .../execution}/portfolio_risk.py | 0 {modules => _archive/modules}/apex_config.py | 0 {modules => _archive/modules}/apex_engine.py | 0 {modules => _archive/modules}/apex_state.py | 0 {modules => _archive/modules}/guard_bridge.py | 0 {modules => _archive/modules}/guard_config.py | 0 {modules => _archive/modules}/guard_state.py | 0 {modules => _archive/modules}/pulse_config.py | 0 {modules => _archive/modules}/pulse_engine.py | 0 {modules => _archive/modules}/pulse_guard.py | 0 {modules => _archive/modules}/pulse_state.py | 0 {modules => _archive/modules}/radar_config.py | 0 {modules => _archive/modules}/radar_engine.py | 0 {modules => _archive/modules}/radar_guard.py | 0 {modules => _archive/modules}/radar_state.py | 0 .../modules}/radar_technicals.py | 0 .../modules}/reflect_adapter.py | 0 .../modules}/reflect_convergence.py | 0 .../modules}/reflect_reporter.py | 0 .../quoting_engine}/__init__.py | 0 .../quoting_engine}/config.py | 0 .../configs/events/default_calendar.yaml | 0 .../quoting_engine}/configs/funding_rate.yaml | 0 .../quoting_engine}/configs/us3m.yaml | 0 .../quoting_engine}/configs/vxxn.yaml | 0 .../quoting_engine}/engine.py | 0 .../quoting_engine}/event_schedule.py | 0 .../quoting_engine}/fair_value.py | 0 .../quoting_engine}/feeds/__init__.py | 0 .../quoting_engine}/feeds/base.py | 0 .../quoting_engine}/feeds/funding_rate.py | 0 .../quoting_engine}/feeds/microprice.py | 0 .../quoting_engine}/feeds/oracle_monitor.py | 0 .../quoting_engine}/feeds/seda_oracle.py | 0 .../quoting_engine}/inventory.py | 0 .../quoting_engine}/ladder.py | 0 .../quoting_engine}/metrics.py | 0 .../quoting_engine}/spread.py | 0 .../quoting_engine}/toxicity.py | 0 .../quoting_engine}/vol_estimator.py | 0 .../scripts}/backtest_apex.py | 0 .../scripts}/run_protected_mm.sh | 0 {skills => _archive/skills}/apex/SKILL.md | 0 .../skills}/apex/scripts/standalone_runner.py | 0 {skills => _archive/skills}/guard/SKILL.md | 0 .../guard/scripts/standalone_runner.py | 0 {skills => _archive/skills}/pulse/SKILL.md | 0 .../pulse/scripts/standalone_runner.py | 0 {skills => _archive/skills}/radar/SKILL.md | 0 .../radar/scripts/standalone_runner.py | 0 {skills => _archive/skills}/reflect/SKILL.md | 0 {tasks => _archive/tasks}/todo.md | 0 {tests => _archive/tests}/test_alo_routing.py | 0 {tests => _archive/tests}/test_apex_engine.py | 0 .../tests}/test_apex_reflect_ops.py | 0 .../tests}/test_backtest_harness.py | 0 {tests => _archive/tests}/test_exchange_sl.py | 0 .../tests}/test_guard_bridge.py | 0 .../tests}/test_guard_implementations.py | 0 .../tests}/test_guard_state_full.py | 0 .../tests}/test_integration_phase3.py | 0 .../tests}/test_integration_phase4.py | 0 .../tests}/test_integration_safety.py | 0 .../tests}/test_market_strategy_routing.py | 0 .../tests}/test_memory_engine.py | 0 .../tests}/test_multi_wallet.py | 0 .../tests}/test_portfolio_risk.py | 0 .../tests}/test_pulse_engine.py | 0 .../tests}/test_radar_engine.py | 0 .../tests}/test_radar_technicals.py | 0 .../tests}/test_reflect_adapter_full.py | 0 .../tests}/test_reflect_convergence.py | 0 .../tests}/test_reflect_engine.py | 0 .../tests}/test_reflect_reporter.py | 0 .../tests}/test_signal_taxonomy.py | 0 {tests => _archive/tests}/test_smart_money.py | 0 .../tests}/test_status_reader.py | 0 .../tests}/test_trailing_stop.py | 0 .../tests}/test_venue_adapter.py | 0 cli/api/status_reader.py | 2 +- cli/commands/hedge.py | 16 +- cli/commands/run.py | 16 - cli/engine.py | 120 +------ cli/hedge_display.py | 2 +- cli/main.py | 10 - cli/mcp_server.py | 84 +---- pyproject.toml | 4 +- scripts/entrypoint.py | 84 +---- scripts/validate_agent_cli.py | 97 +----- strategies/cfi_funding.py | 146 +++++++++ strategies/cfi_hedge.py | 2 +- tests/_archive/README.md | 11 +- .../{ => _archive}/quoting_engine/conftest.py | 9 + .../quoting_engine/test_qe_config.py | 0 .../quoting_engine/test_qe_disagreement.py | 0 .../quoting_engine/test_qe_engine.py | 0 .../test_qe_event_schedule_calendar.py | 0 .../quoting_engine/test_qe_fair_value.py | 0 .../quoting_engine/test_qe_feeds_base.py | 0 .../test_qe_funding_boundary.py | 0 .../test_qe_funding_rate_feed.py | 0 .../quoting_engine/test_qe_fv_band.py | 0 .../quoting_engine/test_qe_inventory.py | 0 .../quoting_engine/test_qe_inventory_caps.py | 0 .../quoting_engine/test_qe_ladder.py | 0 .../quoting_engine/test_qe_liq_advanced.py | 0 .../quoting_engine/test_qe_metrics.py | 0 .../quoting_engine/test_qe_microprice.py | 0 .../quoting_engine/test_qe_oracle_monitor.py | 0 .../quoting_engine/test_qe_regime.py | 0 .../quoting_engine/test_qe_spread.py | 0 .../quoting_engine/test_qe_toxicity.py | 0 .../quoting_engine/test_qe_toxicity_tiered.py | 0 .../quoting_engine/test_qe_vol_estimator.py | 0 tests/test_builder_fee.py | 20 -- tests/test_entrypoint.py | 50 +-- tests/test_mcp_annotations.py | 1 - 156 files changed, 271 insertions(+), 750 deletions(-) create mode 100644 _archive/README.md rename {adapters => _archive/adapters}/__init__.py (100%) rename {adapters => _archive/adapters}/hl_adapter.py (100%) rename {adapters => _archive/adapters}/mock_adapter.py (100%) rename {cli => _archive/cli}/commands/apex.py (100%) rename {cli => _archive/cli}/commands/guard.py (100%) rename {cli => _archive/cli}/commands/pulse.py (100%) rename {cli => _archive/cli}/commands/radar.py (100%) rename {cli => _archive/cli}/commands/reflect.py (100%) rename {configs => _archive/configs}/yex_btcswp.yaml (100%) rename {configs => _archive/configs}/yex_us3m_protected.yaml (100%) rename {configs => _archive/configs}/yex_vxx_protected.yaml (100%) rename {deploy => _archive/deploy}/hermes-railway/Dockerfile (100%) rename {deploy => _archive/deploy}/hermes-railway/package.json (100%) rename {deploy => _archive/deploy}/hermes-railway/railway.toml (78%) rename {deploy => _archive/deploy}/hermes-railway/src/bootstrap.mjs (100%) rename {deploy => _archive/deploy}/hermes-railway/src/gateway.js (100%) rename {deploy => _archive/deploy}/hermes-railway/src/onboard.js (100%) rename {deploy => _archive/deploy}/hermes-railway/src/server.js (100%) rename {deploy => _archive/deploy}/hermes-railway/src/status.js (100%) rename {deploy => _archive/deploy}/hermes-railway/workspace/AGENTS.md (100%) rename {deploy => _archive/deploy}/hermes-railway/workspace/BOOTSTRAP.md (100%) rename {deploy => _archive/deploy}/hermes-railway/workspace/SOUL.md (100%) rename {deploy => _archive/deploy}/hermes-railway/workspace/TOOLS.md (100%) rename {deploy => _archive/deploy}/openclaw-railway/.env.example (100%) rename {deploy => _archive/deploy}/openclaw-railway/Dockerfile (100%) rename {deploy => _archive/deploy}/openclaw-railway/package.json (100%) rename {deploy => _archive/deploy}/openclaw-railway/railway.toml (79%) rename {deploy => _archive/deploy}/openclaw-railway/src/bootstrap.mjs (100%) rename {deploy => _archive/deploy}/openclaw-railway/src/gateway.js (100%) rename {deploy => _archive/deploy}/openclaw-railway/src/onboard.js (100%) rename {deploy => _archive/deploy}/openclaw-railway/src/server.js (100%) rename {deploy => _archive/deploy}/openclaw-railway/src/status.js (100%) rename {deploy => _archive/deploy}/openclaw-railway/workspace/AGENTS.md (100%) rename {deploy => _archive/deploy}/openclaw-railway/workspace/BOOTSTRAP.md (100%) rename {deploy => _archive/deploy}/openclaw-railway/workspace/SOUL.md (100%) rename {deploy => _archive/deploy}/openclaw-railway/workspace/TOOLS.md (100%) rename {docs => _archive/docs}/hl_feature_audit.md (98%) rename {execution => _archive/execution}/portfolio_risk.py (100%) rename {modules => _archive/modules}/apex_config.py (100%) rename {modules => _archive/modules}/apex_engine.py (100%) rename {modules => _archive/modules}/apex_state.py (100%) rename {modules => _archive/modules}/guard_bridge.py (100%) rename {modules => _archive/modules}/guard_config.py (100%) rename {modules => _archive/modules}/guard_state.py (100%) rename {modules => _archive/modules}/pulse_config.py (100%) rename {modules => _archive/modules}/pulse_engine.py (100%) rename {modules => _archive/modules}/pulse_guard.py (100%) rename {modules => _archive/modules}/pulse_state.py (100%) rename {modules => _archive/modules}/radar_config.py (100%) rename {modules => _archive/modules}/radar_engine.py (100%) rename {modules => _archive/modules}/radar_guard.py (100%) rename {modules => _archive/modules}/radar_state.py (100%) rename {modules => _archive/modules}/radar_technicals.py (100%) rename {modules => _archive/modules}/reflect_adapter.py (100%) rename {modules => _archive/modules}/reflect_convergence.py (100%) rename {modules => _archive/modules}/reflect_reporter.py (100%) rename {quoting_engine => _archive/quoting_engine}/__init__.py (100%) rename {quoting_engine => _archive/quoting_engine}/config.py (100%) rename {quoting_engine => _archive/quoting_engine}/configs/events/default_calendar.yaml (100%) rename {quoting_engine => _archive/quoting_engine}/configs/funding_rate.yaml (100%) rename {quoting_engine => _archive/quoting_engine}/configs/us3m.yaml (100%) rename {quoting_engine => _archive/quoting_engine}/configs/vxxn.yaml (100%) rename {quoting_engine => _archive/quoting_engine}/engine.py (100%) rename {quoting_engine => _archive/quoting_engine}/event_schedule.py (100%) rename {quoting_engine => _archive/quoting_engine}/fair_value.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/__init__.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/base.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/funding_rate.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/microprice.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/oracle_monitor.py (100%) rename {quoting_engine => _archive/quoting_engine}/feeds/seda_oracle.py (100%) rename {quoting_engine => _archive/quoting_engine}/inventory.py (100%) rename {quoting_engine => _archive/quoting_engine}/ladder.py (100%) rename {quoting_engine => _archive/quoting_engine}/metrics.py (100%) rename {quoting_engine => _archive/quoting_engine}/spread.py (100%) rename {quoting_engine => _archive/quoting_engine}/toxicity.py (100%) rename {quoting_engine => _archive/quoting_engine}/vol_estimator.py (100%) rename {scripts => _archive/scripts}/backtest_apex.py (100%) rename {scripts => _archive/scripts}/run_protected_mm.sh (100%) rename {skills => _archive/skills}/apex/SKILL.md (100%) rename {skills => _archive/skills}/apex/scripts/standalone_runner.py (100%) rename {skills => _archive/skills}/guard/SKILL.md (100%) rename {skills => _archive/skills}/guard/scripts/standalone_runner.py (100%) rename {skills => _archive/skills}/pulse/SKILL.md (100%) rename {skills => _archive/skills}/pulse/scripts/standalone_runner.py (100%) rename {skills => _archive/skills}/radar/SKILL.md (100%) rename {skills => _archive/skills}/radar/scripts/standalone_runner.py (100%) rename {skills => _archive/skills}/reflect/SKILL.md (100%) rename {tasks => _archive/tasks}/todo.md (100%) rename {tests => _archive/tests}/test_alo_routing.py (100%) rename {tests => _archive/tests}/test_apex_engine.py (100%) rename {tests => _archive/tests}/test_apex_reflect_ops.py (100%) rename {tests => _archive/tests}/test_backtest_harness.py (100%) rename {tests => _archive/tests}/test_exchange_sl.py (100%) rename {tests => _archive/tests}/test_guard_bridge.py (100%) rename {tests => _archive/tests}/test_guard_implementations.py (100%) rename {tests => _archive/tests}/test_guard_state_full.py (100%) rename {tests => _archive/tests}/test_integration_phase3.py (100%) rename {tests => _archive/tests}/test_integration_phase4.py (100%) rename {tests => _archive/tests}/test_integration_safety.py (100%) rename {tests => _archive/tests}/test_market_strategy_routing.py (100%) rename {tests => _archive/tests}/test_memory_engine.py (100%) rename {tests => _archive/tests}/test_multi_wallet.py (100%) rename {tests => _archive/tests}/test_portfolio_risk.py (100%) rename {tests => _archive/tests}/test_pulse_engine.py (100%) rename {tests => _archive/tests}/test_radar_engine.py (100%) rename {tests => _archive/tests}/test_radar_technicals.py (100%) rename {tests => _archive/tests}/test_reflect_adapter_full.py (100%) rename {tests => _archive/tests}/test_reflect_convergence.py (100%) rename {tests => _archive/tests}/test_reflect_engine.py (100%) rename {tests => _archive/tests}/test_reflect_reporter.py (100%) rename {tests => _archive/tests}/test_signal_taxonomy.py (100%) rename {tests => _archive/tests}/test_smart_money.py (100%) rename {tests => _archive/tests}/test_status_reader.py (100%) rename {tests => _archive/tests}/test_trailing_stop.py (100%) rename {tests => _archive/tests}/test_venue_adapter.py (100%) create mode 100644 strategies/cfi_funding.py rename tests/{ => _archive}/quoting_engine/conftest.py (78%) rename tests/{ => _archive}/quoting_engine/test_qe_config.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_disagreement.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_engine.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_event_schedule_calendar.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_fair_value.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_feeds_base.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_funding_boundary.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_funding_rate_feed.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_fv_band.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_inventory.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_inventory_caps.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_ladder.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_liq_advanced.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_metrics.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_microprice.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_oracle_monitor.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_regime.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_spread.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_toxicity.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_toxicity_tiered.py (100%) rename tests/{ => _archive}/quoting_engine/test_qe_vol_estimator.py (100%) diff --git a/README.md b/README.md index 4851768..881a8cb 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Strategy Tests License - MCP + MCP

@@ -32,7 +32,9 @@ --- -Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. +Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. K2 inputs come from HL funding history only (no SEDA). Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, or a standalone MCP server. + +Legacy operator stack (APEX, Radar, Pulse, Guard, Reflect, quoting engine, SEDA oracle, Hermes/OpenClaw deploy) was archived 2026-07-02 under `_archive/` — see `_archive/README.md`. --- @@ -79,7 +81,6 @@ export HL_TESTNET=false hl builder approve --mainnet hl hedge propose BTC hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 3 --mainnet -hl apex run --mainnet ``` See [Markets & Instruments](#markets--instruments) for native perps, Paragon BTCSWP, and YEX symbols. @@ -169,28 +170,18 @@ MCP tools: `funding_hedge_propose`, `funding_hedge_execute`, `funding_hedge_back ## Skills -Built on the open [Agent Skills](https://agentskills.io) standard. Each skill is self-contained with instructions, scripts, and references. +Built on the open [Agent Skills](https://agentskills.io) standard. The active tree ships the onboarding skill for first-time local setup. The legacy operator skills were archived with their commands and modules under `_archive/skills/`. | Skill | What it does | Install | |-------|-------------|---------| | **[Onboard](#onboard)** | Step-by-step first-time setup — from zero to first trade. Decision trees, verification at each step, error recovery. | [`SKILL.md`](skills/onboard/SKILL.md) | -| **[APEX Strategy](#apex--autonomous-multi-slot-strategy)** | Fully autonomous 2-3 slot trading. Composes Radar + Pulse + Guard. Proven on testnet: signal detection, entry, trailing stop, exit. | [`SKILL.md`](skills/apex/SKILL.md) | -| **[Radar](#radar--opportunity-radar)** | 4-stage funnel screening all HL perps. Scores 0-400 across market structure, technicals, funding, and BTC macro. | [`SKILL.md`](skills/radar/SKILL.md) | -| **[Pulse](#pulse--emerging-pulse-detector)** | Detects sudden capital inflow via OI delta, volume surge, funding flips. IMMEDIATE signals at 100 confidence. | [`SKILL.md`](skills/pulse/SKILL.md) | -| **[Guard (Dynamic Stop Loss)](#guard--dynamic-stop-loss)** | 2-phase trailing stop with tiered profit-locking. ROE-based triggers that auto-account for leverage. | [`SKILL.md`](skills/guard/SKILL.md) | -| **[REFLECT](#reflect--performance-review)** | Nightly self-improvement loop. Analyzes every trade, finds patterns, generates actionable recommendations. | [`SKILL.md`](skills/reflect/SKILL.md) | -### Install a skill (agents) +### Install the active skill Grab the raw URL and go: ``` https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/onboard/SKILL.md -https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/apex/SKILL.md -https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/radar/SKILL.md -https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/pulse/SKILL.md -https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/guard/SKILL.md -https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/reflect/SKILL.md ``` ### Install a skill (OpenClaw / ClawHub) @@ -210,7 +201,7 @@ cp ~/agent-cli/cli/skill.md ~/.claude/skills/yex-trader/SKILL.md --- -## Autonomous Trading Stack +## Active Operator Surface ### Onboard @@ -229,240 +220,22 @@ hl hedge propose BTC --dry-run # Preview CFI v2 hedge proposal --- -### Guard — Dynamic Stop Loss - -Trailing stop system with tiered profit-locking. Protects profits while letting winners run. - -**Two phases:** -- **Phase 1 (Let it breathe)** — Wide retrace tolerance while position builds. Auto-cut at 90 min if no graduation; weak-peak early cut at 45 min if peak ROE < 3%. -- **Phase 2 (Lock the bag)** — Tiered profit floors that ratchet up as ROE grows. Exchange-level stop loss synced to Hyperliquid as crash safety net. - -| Preset | Phase 1 Retrace | Tiers | Stagnation TP | -|--------|----------------|-------|---------------| -| `moderate` | 3% | 6 tiers (10-100% ROE) | No | -| `tight` | 5% | 4 tiers (10-75% ROE) | Yes (8% ROE, 1h) | - -```bash -hl guard run -i ETH-PERP --preset tight -``` - -**[Download SKILL.md](skills/guard/SKILL.md)** - ---- - -### Radar — Opportunity Radar - -Multi-factor screening engine that evaluates all HL perps for trade setups. 4-stage funnel, scores 0-400. - -| Pillar | Weight | Signals | -|--------|--------|---------| -| Market Structure | 35 | Volume, OI, liquidity | -| Technicals | 30 | RSI, EMA, patterns, hourly trend | -| Funding | 20 | Rate extremes, direction bias | -| BTC Macro | 15 | Trend alignment, regime filter | - -```bash -hl radar once --mock # Single scan -hl radar run --mock # Continuous (every 15 min) -``` - -**[Download SKILL.md](skills/radar/SKILL.md)** - ---- - -### Pulse — Emerging Momentum Detector - -Detects assets with sudden capital inflow using OI, volume, funding, and price signals. Runs every 60 seconds. +### CFI hedge commands -**5-tier signal taxonomy** for entry classification, plus informational signals for Radar scoring: - -| Tier | Signal | Trigger | Confidence | -|------|--------|---------|------------| -| 1 | `FIRST_JUMP` | First asset in sector with OI + volume breakout | 100 | -| 2 | `CONTRIB_EXPLOSION` | OI +15% **AND** volume 5x (simultaneous extreme) | 95 | -| 3 | `IMMEDIATE_MOVER` | OI +15% **OR** volume 5x (either extreme) | 80 | -| 4 | `NEW_ENTRY_DEEP` | OI grows 8%+ but volume stays low — smart money accumulation | 65 | -| 5 | `DEEP_CLIMBER` | Sustained OI climb 5%+ per window over 3+ consecutive scans | 55 | -| — | `VOLUME_SURGE` | 4h volume / average > 3x | 70 | -| — | `OI_BREAKOUT` | OI jumps 8%+ above baseline | 60 | -| — | `FUNDING_FLIP` | Funding rate reverses or accelerates 50%+ | 50 | +The active trading path is CFI v2 funding-rate hedging. Use `hl hedge ...` for proposals, execution, status, automation, and backtests, or `hl run cfi_hedge ...` to run the same logic through the standard strategy loop. ```bash -hl pulse once --mock # Single scan -hl pulse run --mock # Continuous (every 60s) -``` - -**[Download SKILL.md](skills/pulse/SKILL.md)** - ---- - -### APEX — Autonomous Multi-Slot Strategy - -The top-level orchestrator. Composes Radar + Pulse + Guard into a single autonomous strategy managing 2-3 concurrent positions. - -**Tick schedule** (60s base): -- Every tick: Fetch prices, update ROEs, check Guard, run Pulse, evaluate entry/exit -- Every 5 ticks: Watchdog health check -- Every 15 ticks: Run opportunity radar - -**Entry priority** (tier-based): - -| Priority | Source | Condition | -|----------|--------|-----------| -| 1 | FIRST_JUMP | First sector mover (tier 1) | -| 2 | CONTRIB_EXPLOSION | Simultaneous extreme OI + volume (tier 2) | -| 3 | Smart money | Pulse confidence > 90 | -| 4 | IMMEDIATE_MOVER | Either extreme metric (tier 3) | -| 5 | Radar | Score > 170 | -| 6 | NEW_ENTRY_DEEP | Limit-order accumulation (tier 4) | -| 7 | DEEP_CLIMBER | Sustained OI trend (tier 5) | - -**Presets:** - -| Preset | Slots | Leverage | Radar Threshold | Daily Loss Limit | -|--------|-------|----------|-------------------|------------------| -| `default` | 3 | 10x | 170 | $500 | -| `conservative` | 2 | 5x | 190 | $250 | -| `aggressive` | 3 | 15x | 150 | $1,000 | - -```bash -hl apex run --mock --max-ticks 10 # Mock test -hl apex run # Live testnet -hl apex run --preset conservative --mainnet # Live mainnet -``` - -**[Download SKILL.md](skills/apex/SKILL.md)** - ---- - -### REFLECT — Performance Review - -Nightly self-improvement loop. Reads trade history, computes metrics, detects patterns, generates actionable recommendations. - -| Metric | Description | -|--------|-------------| -| Win Rate | % of round trips with positive net PnL | -| FDR | Fee Drag Ratio — fees as % of gross wins | -| Direction Split | Long vs short win rates and PnL | -| Holding Periods | Bucketed by <5m, 5-15m, 15-60m, 1-4h, 4h+ | -| Monster Dependency | % of net PnL from best single trade | - -```bash -hl reflect run --since 2026-03-01 -hl reflect report -hl reflect history -n 10 -``` - -**[Download SKILL.md](skills/reflect/SKILL.md)** - -### REFLECT Self-Improvement Loop - -When running inside APEX, REFLECT executes automatically every 240 ticks (~4 hours) and at a configurable UTC hour (default 04:00). It reads the trade log, computes performance metrics, and **auto-adjusts APEX parameters** based on findings: - -| Finding | Automatic Adjustment | -|---------|---------------------| -| FDR > 30% (fees eating profits) | Raise radar threshold, disable immediate mover entries | -| Win rate < 40% | Tighten both radar and movers confidence thresholds | -| 5+ consecutive losses | Reduce daily loss limit by 20% | -| Direction imbalance (e.g. longs losing) | Limit same-direction slots | -| Fees exceed gross PnL | **Emergency mode**: disable auto-entries, raise all thresholds | -| Profitable + healthy | Slightly relax thresholds toward defaults | - -All adjustments have guardrail bounds — parameters can't swing wildly. Disable with `reflect_auto_adjust: false` in APEX config. - -**Scheduled tasks** (built into APEX tick loop): -- **Daily PnL reset** at UTC midnight — clears daily loss tracking -- **REFLECT comprehensive report** at UTC 04:00 — full performance review with markdown report saved to `data/apex/reflect/` - ---- - -### Production Safety - -Built-in safety systems that protect positions even when the runner process crashes. - -#### Exchange-Level Stop Loss Sync - -Guard places a **trigger order directly on Hyperliquid** as a safety net. If the runner crashes, the exchange-side stop loss remains active. Synced on entry, tier ratchet, and startup — intentionally left in place on shutdown. - -``` -Position Entry → Place SL trigger order at Phase 1 floor -Tier Ratchet → Cancel old SL, place new at higher tier floor -Position Close → Cancel SL trigger order -Runner Crash → Exchange SL stays active (that's the point) -``` - -#### Clearinghouse Reconciliation - -Bidirectional reconciliation between APEX slots and Hyperliquid positions. Detects orphaned exchange positions, orphaned slots, and size mismatches. Runs on startup and periodically via watchdog. - -```bash -hl apex reconcile # Check for discrepancies -hl apex reconcile --fix # Auto-adopt orphans, fix sizes -``` - -| Discrepancy | Severity | Auto-Fix | -|-------------|----------|----------| -| Orphan exchange position | Critical | Adopt into empty slot + create Guard | -| Orphan slot (no position) | Warning | Mark slot closed | -| Size mismatch >10% | Critical | Update slot to match exchange | - -#### Risk Guardian - -Graduated risk response with three states and automatic transitions: - -``` -OPEN ──(2 consecutive losses)──→ COOLDOWN ──(trigger again)──→ CLOSED - ↑ │ │ - └──────(auto-expiry 30 min)────────┘ │ - └────────────────────(daily reset)──────────────────────────────┘ -``` - -| State | Entries | Exits | Trigger | -|-------|---------|-------|---------| -| `OPEN` | Allowed | Allowed | Default | -| `COOLDOWN` | **Blocked** | Allowed | 2+ consecutive losses or drawdown >= 50% of limit | -| `CLOSED` | **Blocked** | **Blocked** | Daily loss limit hit | - -Exchange-level stop losses remain active in all states. - -#### Rotation Cooldown - -Anti-churn protection: -- **Minimum hold (45 min)** — Conviction collapse and stagnation exits blocked until 45 min. Guard hard stops and daily loss still override. -- **Slot cooldown (5 min)** — Closed slots can't be reused for 5 minutes. - -#### State Archiving - -Closed position state files archived to `data/archive/{YYYY-MM-DD}/` on close. Trade audit trail (`trades.jsonl`) is never archived. - -```bash -hl apex archive # Archive all closed state files -hl apex archive --days 7 # Only older than 7 days -hl apex archive --dry-run # Preview without moving -``` - -#### ALO Fee Optimization - -Entry orders default to **ALO (post-only)** for maker rebates (~3 bps savings per round-trip). Falls back to GTC if ALO is rejected. Exits and Guard closes always use IOC. - ---- - -### Autoresearch-Powered REFLECT - -Connects REFLECT to an autonomous optimization loop. A backtest harness replays historical trades against config variants, and an iterative agent loop finds parameter improvements. - -```bash -python3 scripts/backtest_apex.py --config apex_config.json --trades data/cli/trades.jsonl +hl hedge propose BTC --dry-run +hl hedge execute BTC --dry-run +hl hedge status --coin BTC --watch +hl hedge auto --coins BTC --dry-run +hl hedge backtest --coin BTC --days 30 +hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 5 ``` -REFLECT auto-generates research directions: +### Archived legacy stack -| Finding | Suggested Direction | -|---------|-------------------| -| FDR > 30% | Raise `radar_score_threshold` in [170, 250] | -| Win rate < 40% | Sweep `pulse_confidence_threshold` in [70, 95] | -| Direction imbalance | Set `max_same_direction` to 1 | -| Healthy + profitable | Try lowering `radar_score_threshold` in [140, 170] | +APEX, Radar, Pulse, Guard, Reflect, the quoting engine, SEDA oracle, and Hermes/OpenClaw deploy templates are archived under `_archive/`. Their historical docs, commands, modules, skills, scripts, and tests remain there for reference, but those commands are not registered by `cli/main.py` and their tools are not exposed by the active MCP server. --- @@ -478,15 +251,6 @@ hl account # Show HL account state hl strategies # List all strategies hl skills list # Discover installed skills -# Autonomous stack -hl apex run [options] # APEX multi-slot orchestrator -hl apex reconcile [--fix] # Reconcile state vs exchange -hl apex archive [--days N] # Archive closed state files -hl radar run [options] # Opportunity radar -hl pulse run [options] # Pulse momentum detector -hl guard run -i ETH-PERP [options] # Guard trailing stop -hl reflect run [--since DATE] # Performance review - # Infrastructure hl builder approve [--mainnet] # Approve builder fee hl wallet auto [--save-env] # Create wallet (agent-friendly) @@ -507,7 +271,9 @@ hl mcp serve # stdio transport (default) hl mcp serve --transport sse # SSE transport ``` -**24 MCP tools** for account state, trading, CFI funding hedges, APEX/Radar/REFLECT, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. +**20 MCP tools** for account state, trading, CFI funding hedges, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), order/funding reads, and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. + +Active tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`. Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. @@ -524,7 +290,7 @@ mcp_servers: # cwd: /path/to/agent-cli # if not launched from the repo root ``` -This is exactly what `deploy/hermes-railway` auto-generates on boot (alongside `platform_toolsets`), so a deployed Hermes agent is wired out of the box — no OpenClaw-specific assumptions anywhere in the MCP server. +The archived Hermes template under `_archive/deploy/hermes-railway` generated this shape for legacy experiments, but the active server is harness-neutral and does not expose the archived APEX/Radar/Pulse/Guard/Reflect toolset. ### HTTP API & SSE @@ -549,14 +315,14 @@ defaults to `RUN_MODE=mcp`; Nunchi operates this runner pool behind | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `HL_TESTNET` | No | `true` | `true` for testnet, `false` for mainnet | -| `RUN_MODE` | No | `mcp` | `mcp` for the hosted tools runtime; `apex`/`strategy` are local or legacy operator modes | +| `RUN_MODE` | No | `mcp` | `mcp` for the hosted tools runtime; `strategy` for local `cfi_hedge` loops. Legacy `apex` falls back to `strategy`. | | `DATA_DIR` | No | `/data` | Persistent ledgers, wallet state, and metering data | | `NUNCHI_METERING_URL` | No | — | Generic web-auth metering upload endpoint when the runner reports usage | | `NUNCHI_METERING_TOKEN` | No | — | Metering bearer token issued by web-auth/gateway config | **Run modes:** - **mcp** (default) — SSE MCP server for the shared Railway tools runtime. -- **apex** / **strategy** — direct autonomous loops for local/self-hosted operators, not the Nunchi subscription product path. +- **strategy** — local/self-hosted `cfi_hedge` loop, not the Nunchi subscription product path. Legacy `apex`/`wolf` values are deprecated and fall back to `strategy`. ### Legacy Agent Templates @@ -565,8 +331,8 @@ self-host templates. They are not part of the new hosted MCP subscription architecture because they provision user-facing conversational/autonomous agents. Do not expose them as the paid Nunchi product path. -For legacy experiments, use `deploy/openclaw-railway/railway.toml` or -`deploy/hermes-railway/railway.toml` as the config-as-code path and keep the +For legacy experiments, use `_archive/deploy/openclaw-railway/railway.toml` or +`_archive/deploy/hermes-railway/railway.toml` as the config-as-code path and keep the Railway build root at the repo root. --- @@ -575,28 +341,17 @@ Railway build root at the repo root. ``` cli/ CLI commands and trading engine - commands/ Subcommand modules (run, apex, radar, pulse, guard, reflect, house, ...) - mcp_server.py MCP server (24 tools via FastMCP) + commands/ Active subcommands (run, hedge, wallet, setup, mcp, skills, journal, keys, margin, trading, house, policy, ...) + mcp_server.py MCP server (20 tools via FastMCP) hl_adapter.py Direct HL API adapter (live + mock) builder_fee.py Builder fee config (HL native BuilderInfo) keystore.py Encrypted keystore (geth-compatible) strategy_registry.py Strategy + HIP-3 market definitions (YEX, OSRS, PARA) strategies/ cfi_hedge + archived legacy strategies (_archive/) -modules/ Pure logic modules (zero I/O) - apex_engine.py APEX decision engine - radar_engine.py Opportunity radar - pulse_engine.py Pulse momentum detector (5-tier signal taxonomy) - trailing_stop.py Guard trailing stop (Phase 1 auto-cut) - reflect_engine.py Performance analysis - reconciliation.py Clearinghouse reconciliation engine - archiver.py State file archiving +modules/ Active shared logic modules; archived APEX/Radar/Pulse/Guard/Reflect modules live under _archive/modules/ skills/ Agent Skills (SKILL.md + runners) onboard/ First-time setup guide - apex/ APEX orchestrator - radar/ Opportunity radar - pulse/ Pulse momentum detector - guard/ Dynamic stop loss - reflect/ Performance review +_archive/ Legacy operator stack, skills, deploy templates, scripts, docs, and tests sdk/ Strategy base class and model registry parent/ HL API proxy, position tracking, risk management scripts/ Backtest harness, bootstrap diff --git a/_archive/README.md b/_archive/README.md new file mode 100644 index 0000000..0d4d75e --- /dev/null +++ b/_archive/README.md @@ -0,0 +1,34 @@ +# Archived legacy components + +**Date:** 2026-07-02 +**Reason:** Product focus narrowed to CFI v2 funding-rate hedge only (`hl hedge`, `cfi_hedge` strategy). The multi-slot APEX orchestrator, Radar/Pulse scanners, Guard trailing stops, Reflect reviews, market-making stack, SEDA oracle, and Hermes/OpenClaw Railway deploy templates are no longer maintained in the active tree. + +## What moved here + +| Path | Description | +|------|-------------| +| `_archive/cli/commands/` | APEX, Radar, Pulse, Guard, Reflect CLI commands | +| `_archive/modules/` | APEX/Radar/Pulse/Guard/Reflect engines and state | +| `_archive/skills/` | Agent skills for the legacy operator stack | +| `_archive/adapters/` | Venue adapter layer for APEX standalone runner | +| `_archive/quoting_engine/` | Full MM stack; includes `feeds/seda_oracle.py` (deprecated) | +| `_archive/deploy/` | Hermes + OpenClaw Railway agent gateway templates | +| `_archive/configs/` | YEX protected-MM configs (US3M, VXX, BTCSWP) | +| `_archive/execution/portfolio_risk.py` | Portfolio-level entry caps for APEX multi-slot mode | +| `_archive/scripts/` | `backtest_apex.py`, `run_protected_mm.sh` | +| `_archive/tests/` | Tests for archived modules and CLI | +| `_archive/docs/hl_feature_audit.md` | Legacy feature audit notes | +| `_archive/tasks/todo.md` | Stale task list | + +## Active replacement + +- **Primary product:** `hl hedge propose|execute|status|auto|backtest` and `hl run cfi_hedge` +- **K2 inputs:** Hyperliquid funding history via `strategies/cfi_funding.py` (no SEDA oracle) +- **MCP:** `funding_hedge_propose`, `funding_hedge_execute`, `funding_hedge_backtest` + +## Running archived tests manually + +```bash +PYTHONPATH=_archive:. pytest tests/_archive/quoting_engine/ -v +PYTHONPATH=. pytest _archive/tests/ -v +``` diff --git a/adapters/__init__.py b/_archive/adapters/__init__.py similarity index 100% rename from adapters/__init__.py rename to _archive/adapters/__init__.py diff --git a/adapters/hl_adapter.py b/_archive/adapters/hl_adapter.py similarity index 100% rename from adapters/hl_adapter.py rename to _archive/adapters/hl_adapter.py diff --git a/adapters/mock_adapter.py b/_archive/adapters/mock_adapter.py similarity index 100% rename from adapters/mock_adapter.py rename to _archive/adapters/mock_adapter.py diff --git a/cli/commands/apex.py b/_archive/cli/commands/apex.py similarity index 100% rename from cli/commands/apex.py rename to _archive/cli/commands/apex.py diff --git a/cli/commands/guard.py b/_archive/cli/commands/guard.py similarity index 100% rename from cli/commands/guard.py rename to _archive/cli/commands/guard.py diff --git a/cli/commands/pulse.py b/_archive/cli/commands/pulse.py similarity index 100% rename from cli/commands/pulse.py rename to _archive/cli/commands/pulse.py diff --git a/cli/commands/radar.py b/_archive/cli/commands/radar.py similarity index 100% rename from cli/commands/radar.py rename to _archive/cli/commands/radar.py diff --git a/cli/commands/reflect.py b/_archive/cli/commands/reflect.py similarity index 100% rename from cli/commands/reflect.py rename to _archive/cli/commands/reflect.py diff --git a/configs/yex_btcswp.yaml b/_archive/configs/yex_btcswp.yaml similarity index 100% rename from configs/yex_btcswp.yaml rename to _archive/configs/yex_btcswp.yaml diff --git a/configs/yex_us3m_protected.yaml b/_archive/configs/yex_us3m_protected.yaml similarity index 100% rename from configs/yex_us3m_protected.yaml rename to _archive/configs/yex_us3m_protected.yaml diff --git a/configs/yex_vxx_protected.yaml b/_archive/configs/yex_vxx_protected.yaml similarity index 100% rename from configs/yex_vxx_protected.yaml rename to _archive/configs/yex_vxx_protected.yaml diff --git a/deploy/hermes-railway/Dockerfile b/_archive/deploy/hermes-railway/Dockerfile similarity index 100% rename from deploy/hermes-railway/Dockerfile rename to _archive/deploy/hermes-railway/Dockerfile diff --git a/deploy/hermes-railway/package.json b/_archive/deploy/hermes-railway/package.json similarity index 100% rename from deploy/hermes-railway/package.json rename to _archive/deploy/hermes-railway/package.json diff --git a/deploy/hermes-railway/railway.toml b/_archive/deploy/hermes-railway/railway.toml similarity index 78% rename from deploy/hermes-railway/railway.toml rename to _archive/deploy/hermes-railway/railway.toml index a1db35e..16afa37 100644 --- a/deploy/hermes-railway/railway.toml +++ b/_archive/deploy/hermes-railway/railway.toml @@ -2,8 +2,8 @@ builder = "dockerfile" # Build context is the repo root so the Dockerfile can COPY the agent-cli source. # Set this service's Root Directory to the repo root (/) in Railway, not -# deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "deploy/hermes-railway/Dockerfile" +# archive/deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package. +dockerfilePath = "archive/deploy/hermes-railway/Dockerfile" [deploy] healthcheckPath = "/health" diff --git a/deploy/hermes-railway/src/bootstrap.mjs b/_archive/deploy/hermes-railway/src/bootstrap.mjs similarity index 100% rename from deploy/hermes-railway/src/bootstrap.mjs rename to _archive/deploy/hermes-railway/src/bootstrap.mjs diff --git a/deploy/hermes-railway/src/gateway.js b/_archive/deploy/hermes-railway/src/gateway.js similarity index 100% rename from deploy/hermes-railway/src/gateway.js rename to _archive/deploy/hermes-railway/src/gateway.js diff --git a/deploy/hermes-railway/src/onboard.js b/_archive/deploy/hermes-railway/src/onboard.js similarity index 100% rename from deploy/hermes-railway/src/onboard.js rename to _archive/deploy/hermes-railway/src/onboard.js diff --git a/deploy/hermes-railway/src/server.js b/_archive/deploy/hermes-railway/src/server.js similarity index 100% rename from deploy/hermes-railway/src/server.js rename to _archive/deploy/hermes-railway/src/server.js diff --git a/deploy/hermes-railway/src/status.js b/_archive/deploy/hermes-railway/src/status.js similarity index 100% rename from deploy/hermes-railway/src/status.js rename to _archive/deploy/hermes-railway/src/status.js diff --git a/deploy/hermes-railway/workspace/AGENTS.md b/_archive/deploy/hermes-railway/workspace/AGENTS.md similarity index 100% rename from deploy/hermes-railway/workspace/AGENTS.md rename to _archive/deploy/hermes-railway/workspace/AGENTS.md diff --git a/deploy/hermes-railway/workspace/BOOTSTRAP.md b/_archive/deploy/hermes-railway/workspace/BOOTSTRAP.md similarity index 100% rename from deploy/hermes-railway/workspace/BOOTSTRAP.md rename to _archive/deploy/hermes-railway/workspace/BOOTSTRAP.md diff --git a/deploy/hermes-railway/workspace/SOUL.md b/_archive/deploy/hermes-railway/workspace/SOUL.md similarity index 100% rename from deploy/hermes-railway/workspace/SOUL.md rename to _archive/deploy/hermes-railway/workspace/SOUL.md diff --git a/deploy/hermes-railway/workspace/TOOLS.md b/_archive/deploy/hermes-railway/workspace/TOOLS.md similarity index 100% rename from deploy/hermes-railway/workspace/TOOLS.md rename to _archive/deploy/hermes-railway/workspace/TOOLS.md diff --git a/deploy/openclaw-railway/.env.example b/_archive/deploy/openclaw-railway/.env.example similarity index 100% rename from deploy/openclaw-railway/.env.example rename to _archive/deploy/openclaw-railway/.env.example diff --git a/deploy/openclaw-railway/Dockerfile b/_archive/deploy/openclaw-railway/Dockerfile similarity index 100% rename from deploy/openclaw-railway/Dockerfile rename to _archive/deploy/openclaw-railway/Dockerfile diff --git a/deploy/openclaw-railway/package.json b/_archive/deploy/openclaw-railway/package.json similarity index 100% rename from deploy/openclaw-railway/package.json rename to _archive/deploy/openclaw-railway/package.json diff --git a/deploy/openclaw-railway/railway.toml b/_archive/deploy/openclaw-railway/railway.toml similarity index 79% rename from deploy/openclaw-railway/railway.toml rename to _archive/deploy/openclaw-railway/railway.toml index bd0e575..50d7479 100644 --- a/deploy/openclaw-railway/railway.toml +++ b/_archive/deploy/openclaw-railway/railway.toml @@ -2,8 +2,8 @@ builder = "dockerfile" # Build context is the repo root so the Dockerfile can COPY the agent-cli source. # Set this service's Root Directory to the repo root (/) in Railway, not -# deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "deploy/openclaw-railway/Dockerfile" +# archive/deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package. +dockerfilePath = "archive/deploy/openclaw-railway/Dockerfile" [deploy] healthcheckPath = "/health" diff --git a/deploy/openclaw-railway/src/bootstrap.mjs b/_archive/deploy/openclaw-railway/src/bootstrap.mjs similarity index 100% rename from deploy/openclaw-railway/src/bootstrap.mjs rename to _archive/deploy/openclaw-railway/src/bootstrap.mjs diff --git a/deploy/openclaw-railway/src/gateway.js b/_archive/deploy/openclaw-railway/src/gateway.js similarity index 100% rename from deploy/openclaw-railway/src/gateway.js rename to _archive/deploy/openclaw-railway/src/gateway.js diff --git a/deploy/openclaw-railway/src/onboard.js b/_archive/deploy/openclaw-railway/src/onboard.js similarity index 100% rename from deploy/openclaw-railway/src/onboard.js rename to _archive/deploy/openclaw-railway/src/onboard.js diff --git a/deploy/openclaw-railway/src/server.js b/_archive/deploy/openclaw-railway/src/server.js similarity index 100% rename from deploy/openclaw-railway/src/server.js rename to _archive/deploy/openclaw-railway/src/server.js diff --git a/deploy/openclaw-railway/src/status.js b/_archive/deploy/openclaw-railway/src/status.js similarity index 100% rename from deploy/openclaw-railway/src/status.js rename to _archive/deploy/openclaw-railway/src/status.js diff --git a/deploy/openclaw-railway/workspace/AGENTS.md b/_archive/deploy/openclaw-railway/workspace/AGENTS.md similarity index 100% rename from deploy/openclaw-railway/workspace/AGENTS.md rename to _archive/deploy/openclaw-railway/workspace/AGENTS.md diff --git a/deploy/openclaw-railway/workspace/BOOTSTRAP.md b/_archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md similarity index 100% rename from deploy/openclaw-railway/workspace/BOOTSTRAP.md rename to _archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md diff --git a/deploy/openclaw-railway/workspace/SOUL.md b/_archive/deploy/openclaw-railway/workspace/SOUL.md similarity index 100% rename from deploy/openclaw-railway/workspace/SOUL.md rename to _archive/deploy/openclaw-railway/workspace/SOUL.md diff --git a/deploy/openclaw-railway/workspace/TOOLS.md b/_archive/deploy/openclaw-railway/workspace/TOOLS.md similarity index 100% rename from deploy/openclaw-railway/workspace/TOOLS.md rename to _archive/deploy/openclaw-railway/workspace/TOOLS.md diff --git a/docs/hl_feature_audit.md b/_archive/docs/hl_feature_audit.md similarity index 98% rename from docs/hl_feature_audit.md rename to _archive/docs/hl_feature_audit.md index 44f3bab..f0f1692 100644 --- a/docs/hl_feature_audit.md +++ b/_archive/docs/hl_feature_audit.md @@ -172,7 +172,7 @@ These read `snapshot.funding_rate` in their trading logic but don't import any H **Tightly coupled (quoting engine + HyperliquidFundingRate, 4 strategies):** `engine_mm`, `funding_arb`, `regime_mm`, `liquidation_mm` -These directly import `HyperliquidFundingRate` from the quoting engine and require `_engine_base.py` (path hack to `~/Tee-work-/quoting_engine`). Porting requires refactoring the quoting engine's funding rate interface. +These directly import `HyperliquidFundingRate` from the quoting engine (now under `archive/quoting_engine/`) and require `_engine_base.py` (path hack to `~/Tee-work-/quoting_engine`). Porting requires refactoring the quoting engine's funding rate interface. ### Highest-Priority Abstractions for Multi-Exchange Support diff --git a/execution/portfolio_risk.py b/_archive/execution/portfolio_risk.py similarity index 100% rename from execution/portfolio_risk.py rename to _archive/execution/portfolio_risk.py diff --git a/modules/apex_config.py b/_archive/modules/apex_config.py similarity index 100% rename from modules/apex_config.py rename to _archive/modules/apex_config.py diff --git a/modules/apex_engine.py b/_archive/modules/apex_engine.py similarity index 100% rename from modules/apex_engine.py rename to _archive/modules/apex_engine.py diff --git a/modules/apex_state.py b/_archive/modules/apex_state.py similarity index 100% rename from modules/apex_state.py rename to _archive/modules/apex_state.py diff --git a/modules/guard_bridge.py b/_archive/modules/guard_bridge.py similarity index 100% rename from modules/guard_bridge.py rename to _archive/modules/guard_bridge.py diff --git a/modules/guard_config.py b/_archive/modules/guard_config.py similarity index 100% rename from modules/guard_config.py rename to _archive/modules/guard_config.py diff --git a/modules/guard_state.py b/_archive/modules/guard_state.py similarity index 100% rename from modules/guard_state.py rename to _archive/modules/guard_state.py diff --git a/modules/pulse_config.py b/_archive/modules/pulse_config.py similarity index 100% rename from modules/pulse_config.py rename to _archive/modules/pulse_config.py diff --git a/modules/pulse_engine.py b/_archive/modules/pulse_engine.py similarity index 100% rename from modules/pulse_engine.py rename to _archive/modules/pulse_engine.py diff --git a/modules/pulse_guard.py b/_archive/modules/pulse_guard.py similarity index 100% rename from modules/pulse_guard.py rename to _archive/modules/pulse_guard.py diff --git a/modules/pulse_state.py b/_archive/modules/pulse_state.py similarity index 100% rename from modules/pulse_state.py rename to _archive/modules/pulse_state.py diff --git a/modules/radar_config.py b/_archive/modules/radar_config.py similarity index 100% rename from modules/radar_config.py rename to _archive/modules/radar_config.py diff --git a/modules/radar_engine.py b/_archive/modules/radar_engine.py similarity index 100% rename from modules/radar_engine.py rename to _archive/modules/radar_engine.py diff --git a/modules/radar_guard.py b/_archive/modules/radar_guard.py similarity index 100% rename from modules/radar_guard.py rename to _archive/modules/radar_guard.py diff --git a/modules/radar_state.py b/_archive/modules/radar_state.py similarity index 100% rename from modules/radar_state.py rename to _archive/modules/radar_state.py diff --git a/modules/radar_technicals.py b/_archive/modules/radar_technicals.py similarity index 100% rename from modules/radar_technicals.py rename to _archive/modules/radar_technicals.py diff --git a/modules/reflect_adapter.py b/_archive/modules/reflect_adapter.py similarity index 100% rename from modules/reflect_adapter.py rename to _archive/modules/reflect_adapter.py diff --git a/modules/reflect_convergence.py b/_archive/modules/reflect_convergence.py similarity index 100% rename from modules/reflect_convergence.py rename to _archive/modules/reflect_convergence.py diff --git a/modules/reflect_reporter.py b/_archive/modules/reflect_reporter.py similarity index 100% rename from modules/reflect_reporter.py rename to _archive/modules/reflect_reporter.py diff --git a/quoting_engine/__init__.py b/_archive/quoting_engine/__init__.py similarity index 100% rename from quoting_engine/__init__.py rename to _archive/quoting_engine/__init__.py diff --git a/quoting_engine/config.py b/_archive/quoting_engine/config.py similarity index 100% rename from quoting_engine/config.py rename to _archive/quoting_engine/config.py diff --git a/quoting_engine/configs/events/default_calendar.yaml b/_archive/quoting_engine/configs/events/default_calendar.yaml similarity index 100% rename from quoting_engine/configs/events/default_calendar.yaml rename to _archive/quoting_engine/configs/events/default_calendar.yaml diff --git a/quoting_engine/configs/funding_rate.yaml b/_archive/quoting_engine/configs/funding_rate.yaml similarity index 100% rename from quoting_engine/configs/funding_rate.yaml rename to _archive/quoting_engine/configs/funding_rate.yaml diff --git a/quoting_engine/configs/us3m.yaml b/_archive/quoting_engine/configs/us3m.yaml similarity index 100% rename from quoting_engine/configs/us3m.yaml rename to _archive/quoting_engine/configs/us3m.yaml diff --git a/quoting_engine/configs/vxxn.yaml b/_archive/quoting_engine/configs/vxxn.yaml similarity index 100% rename from quoting_engine/configs/vxxn.yaml rename to _archive/quoting_engine/configs/vxxn.yaml diff --git a/quoting_engine/engine.py b/_archive/quoting_engine/engine.py similarity index 100% rename from quoting_engine/engine.py rename to _archive/quoting_engine/engine.py diff --git a/quoting_engine/event_schedule.py b/_archive/quoting_engine/event_schedule.py similarity index 100% rename from quoting_engine/event_schedule.py rename to _archive/quoting_engine/event_schedule.py diff --git a/quoting_engine/fair_value.py b/_archive/quoting_engine/fair_value.py similarity index 100% rename from quoting_engine/fair_value.py rename to _archive/quoting_engine/fair_value.py diff --git a/quoting_engine/feeds/__init__.py b/_archive/quoting_engine/feeds/__init__.py similarity index 100% rename from quoting_engine/feeds/__init__.py rename to _archive/quoting_engine/feeds/__init__.py diff --git a/quoting_engine/feeds/base.py b/_archive/quoting_engine/feeds/base.py similarity index 100% rename from quoting_engine/feeds/base.py rename to _archive/quoting_engine/feeds/base.py diff --git a/quoting_engine/feeds/funding_rate.py b/_archive/quoting_engine/feeds/funding_rate.py similarity index 100% rename from quoting_engine/feeds/funding_rate.py rename to _archive/quoting_engine/feeds/funding_rate.py diff --git a/quoting_engine/feeds/microprice.py b/_archive/quoting_engine/feeds/microprice.py similarity index 100% rename from quoting_engine/feeds/microprice.py rename to _archive/quoting_engine/feeds/microprice.py diff --git a/quoting_engine/feeds/oracle_monitor.py b/_archive/quoting_engine/feeds/oracle_monitor.py similarity index 100% rename from quoting_engine/feeds/oracle_monitor.py rename to _archive/quoting_engine/feeds/oracle_monitor.py diff --git a/quoting_engine/feeds/seda_oracle.py b/_archive/quoting_engine/feeds/seda_oracle.py similarity index 100% rename from quoting_engine/feeds/seda_oracle.py rename to _archive/quoting_engine/feeds/seda_oracle.py diff --git a/quoting_engine/inventory.py b/_archive/quoting_engine/inventory.py similarity index 100% rename from quoting_engine/inventory.py rename to _archive/quoting_engine/inventory.py diff --git a/quoting_engine/ladder.py b/_archive/quoting_engine/ladder.py similarity index 100% rename from quoting_engine/ladder.py rename to _archive/quoting_engine/ladder.py diff --git a/quoting_engine/metrics.py b/_archive/quoting_engine/metrics.py similarity index 100% rename from quoting_engine/metrics.py rename to _archive/quoting_engine/metrics.py diff --git a/quoting_engine/spread.py b/_archive/quoting_engine/spread.py similarity index 100% rename from quoting_engine/spread.py rename to _archive/quoting_engine/spread.py diff --git a/quoting_engine/toxicity.py b/_archive/quoting_engine/toxicity.py similarity index 100% rename from quoting_engine/toxicity.py rename to _archive/quoting_engine/toxicity.py diff --git a/quoting_engine/vol_estimator.py b/_archive/quoting_engine/vol_estimator.py similarity index 100% rename from quoting_engine/vol_estimator.py rename to _archive/quoting_engine/vol_estimator.py diff --git a/scripts/backtest_apex.py b/_archive/scripts/backtest_apex.py similarity index 100% rename from scripts/backtest_apex.py rename to _archive/scripts/backtest_apex.py diff --git a/scripts/run_protected_mm.sh b/_archive/scripts/run_protected_mm.sh similarity index 100% rename from scripts/run_protected_mm.sh rename to _archive/scripts/run_protected_mm.sh diff --git a/skills/apex/SKILL.md b/_archive/skills/apex/SKILL.md similarity index 100% rename from skills/apex/SKILL.md rename to _archive/skills/apex/SKILL.md diff --git a/skills/apex/scripts/standalone_runner.py b/_archive/skills/apex/scripts/standalone_runner.py similarity index 100% rename from skills/apex/scripts/standalone_runner.py rename to _archive/skills/apex/scripts/standalone_runner.py diff --git a/skills/guard/SKILL.md b/_archive/skills/guard/SKILL.md similarity index 100% rename from skills/guard/SKILL.md rename to _archive/skills/guard/SKILL.md diff --git a/skills/guard/scripts/standalone_runner.py b/_archive/skills/guard/scripts/standalone_runner.py similarity index 100% rename from skills/guard/scripts/standalone_runner.py rename to _archive/skills/guard/scripts/standalone_runner.py diff --git a/skills/pulse/SKILL.md b/_archive/skills/pulse/SKILL.md similarity index 100% rename from skills/pulse/SKILL.md rename to _archive/skills/pulse/SKILL.md diff --git a/skills/pulse/scripts/standalone_runner.py b/_archive/skills/pulse/scripts/standalone_runner.py similarity index 100% rename from skills/pulse/scripts/standalone_runner.py rename to _archive/skills/pulse/scripts/standalone_runner.py diff --git a/skills/radar/SKILL.md b/_archive/skills/radar/SKILL.md similarity index 100% rename from skills/radar/SKILL.md rename to _archive/skills/radar/SKILL.md diff --git a/skills/radar/scripts/standalone_runner.py b/_archive/skills/radar/scripts/standalone_runner.py similarity index 100% rename from skills/radar/scripts/standalone_runner.py rename to _archive/skills/radar/scripts/standalone_runner.py diff --git a/skills/reflect/SKILL.md b/_archive/skills/reflect/SKILL.md similarity index 100% rename from skills/reflect/SKILL.md rename to _archive/skills/reflect/SKILL.md diff --git a/tasks/todo.md b/_archive/tasks/todo.md similarity index 100% rename from tasks/todo.md rename to _archive/tasks/todo.md diff --git a/tests/test_alo_routing.py b/_archive/tests/test_alo_routing.py similarity index 100% rename from tests/test_alo_routing.py rename to _archive/tests/test_alo_routing.py diff --git a/tests/test_apex_engine.py b/_archive/tests/test_apex_engine.py similarity index 100% rename from tests/test_apex_engine.py rename to _archive/tests/test_apex_engine.py diff --git a/tests/test_apex_reflect_ops.py b/_archive/tests/test_apex_reflect_ops.py similarity index 100% rename from tests/test_apex_reflect_ops.py rename to _archive/tests/test_apex_reflect_ops.py diff --git a/tests/test_backtest_harness.py b/_archive/tests/test_backtest_harness.py similarity index 100% rename from tests/test_backtest_harness.py rename to _archive/tests/test_backtest_harness.py diff --git a/tests/test_exchange_sl.py b/_archive/tests/test_exchange_sl.py similarity index 100% rename from tests/test_exchange_sl.py rename to _archive/tests/test_exchange_sl.py diff --git a/tests/test_guard_bridge.py b/_archive/tests/test_guard_bridge.py similarity index 100% rename from tests/test_guard_bridge.py rename to _archive/tests/test_guard_bridge.py diff --git a/tests/test_guard_implementations.py b/_archive/tests/test_guard_implementations.py similarity index 100% rename from tests/test_guard_implementations.py rename to _archive/tests/test_guard_implementations.py diff --git a/tests/test_guard_state_full.py b/_archive/tests/test_guard_state_full.py similarity index 100% rename from tests/test_guard_state_full.py rename to _archive/tests/test_guard_state_full.py diff --git a/tests/test_integration_phase3.py b/_archive/tests/test_integration_phase3.py similarity index 100% rename from tests/test_integration_phase3.py rename to _archive/tests/test_integration_phase3.py diff --git a/tests/test_integration_phase4.py b/_archive/tests/test_integration_phase4.py similarity index 100% rename from tests/test_integration_phase4.py rename to _archive/tests/test_integration_phase4.py diff --git a/tests/test_integration_safety.py b/_archive/tests/test_integration_safety.py similarity index 100% rename from tests/test_integration_safety.py rename to _archive/tests/test_integration_safety.py diff --git a/tests/test_market_strategy_routing.py b/_archive/tests/test_market_strategy_routing.py similarity index 100% rename from tests/test_market_strategy_routing.py rename to _archive/tests/test_market_strategy_routing.py diff --git a/tests/test_memory_engine.py b/_archive/tests/test_memory_engine.py similarity index 100% rename from tests/test_memory_engine.py rename to _archive/tests/test_memory_engine.py diff --git a/tests/test_multi_wallet.py b/_archive/tests/test_multi_wallet.py similarity index 100% rename from tests/test_multi_wallet.py rename to _archive/tests/test_multi_wallet.py diff --git a/tests/test_portfolio_risk.py b/_archive/tests/test_portfolio_risk.py similarity index 100% rename from tests/test_portfolio_risk.py rename to _archive/tests/test_portfolio_risk.py diff --git a/tests/test_pulse_engine.py b/_archive/tests/test_pulse_engine.py similarity index 100% rename from tests/test_pulse_engine.py rename to _archive/tests/test_pulse_engine.py diff --git a/tests/test_radar_engine.py b/_archive/tests/test_radar_engine.py similarity index 100% rename from tests/test_radar_engine.py rename to _archive/tests/test_radar_engine.py diff --git a/tests/test_radar_technicals.py b/_archive/tests/test_radar_technicals.py similarity index 100% rename from tests/test_radar_technicals.py rename to _archive/tests/test_radar_technicals.py diff --git a/tests/test_reflect_adapter_full.py b/_archive/tests/test_reflect_adapter_full.py similarity index 100% rename from tests/test_reflect_adapter_full.py rename to _archive/tests/test_reflect_adapter_full.py diff --git a/tests/test_reflect_convergence.py b/_archive/tests/test_reflect_convergence.py similarity index 100% rename from tests/test_reflect_convergence.py rename to _archive/tests/test_reflect_convergence.py diff --git a/tests/test_reflect_engine.py b/_archive/tests/test_reflect_engine.py similarity index 100% rename from tests/test_reflect_engine.py rename to _archive/tests/test_reflect_engine.py diff --git a/tests/test_reflect_reporter.py b/_archive/tests/test_reflect_reporter.py similarity index 100% rename from tests/test_reflect_reporter.py rename to _archive/tests/test_reflect_reporter.py diff --git a/tests/test_signal_taxonomy.py b/_archive/tests/test_signal_taxonomy.py similarity index 100% rename from tests/test_signal_taxonomy.py rename to _archive/tests/test_signal_taxonomy.py diff --git a/tests/test_smart_money.py b/_archive/tests/test_smart_money.py similarity index 100% rename from tests/test_smart_money.py rename to _archive/tests/test_smart_money.py diff --git a/tests/test_status_reader.py b/_archive/tests/test_status_reader.py similarity index 100% rename from tests/test_status_reader.py rename to _archive/tests/test_status_reader.py diff --git a/tests/test_trailing_stop.py b/_archive/tests/test_trailing_stop.py similarity index 100% rename from tests/test_trailing_stop.py rename to _archive/tests/test_trailing_stop.py diff --git a/tests/test_venue_adapter.py b/_archive/tests/test_venue_adapter.py similarity index 100% rename from tests/test_venue_adapter.py rename to _archive/tests/test_venue_adapter.py diff --git a/cli/api/status_reader.py b/cli/api/status_reader.py index 1cda7ce..fa017dc 100644 --- a/cli/api/status_reader.py +++ b/cli/api/status_reader.py @@ -2,7 +2,7 @@ Shared utility used by: - scripts/entrypoint.py (imported directly) -- deploy/openclaw-railway/src/server.js (via `python3 -m cli.api.status_reader`) +- archive/deploy/openclaw-railway/src/server.js (via `python3 -m cli.api.status_reader`) """ from __future__ import annotations diff --git a/cli/commands/hedge.py b/cli/commands/hedge.py index ffcfa63..9f83dd7 100644 --- a/cli/commands/hedge.py +++ b/cli/commands/hedge.py @@ -8,8 +8,8 @@ hl hedge backtest --coin BTC [--days N] wrap hedge_calculator.py hl hedge auto [--coins ...] [--dry-run] agent-controlled auto-open loop -All math + oracle access is delegated to `strategies/cfi_hedge.py` and -`quoting_engine/feeds/seda_oracle.py`. Signing + submission are delegated to +All math + funding inputs are delegated to `strategies/cfi_hedge.py` and +`strategies/cfi_funding.py` (HL funding rates only). Signing + submission are delegated to the Hyperliquid Python SDK via `DirectHLProxy.place_order()` — same code path as `hl trade`. No custom signing here. """ @@ -116,8 +116,8 @@ def _build_proposal( Returns (proposal, snapshot) or raises typer.Exit if no position open. """ from strategies.cfi_hedge import build_cfi_hedge_proposal, get_cfi_profile - from quoting_engine.feeds.seda_oracle import ( - fetch_btcswp_snapshot, + from strategies.cfi_funding import ( + fetch_cfi_funding_snapshot, fetch_hl_current_funding_hr, ) @@ -145,7 +145,7 @@ def _build_proposal( raise typer.Exit(1) position = _position_to_summary(raw_pos, coin_override=coin) - snapshot = fetch_btcswp_snapshot(profile) + snapshot = fetch_cfi_funding_snapshot(profile) current_funding = fetch_hl_current_funding_hr(coin) if current_funding is None: # Fall back to oracle's r_ema if HL didn't answer. @@ -303,8 +303,8 @@ def status_cmd( get_cfi_profile, ) from cli.hedge_display import hedge_status_block - from quoting_engine.feeds.seda_oracle import ( - fetch_btcswp_snapshot, + from strategies.cfi_funding import ( + fetch_cfi_funding_snapshot, fetch_hl_current_funding_hr, ) @@ -327,7 +327,7 @@ def _refresh(): live.append({"job": h, "snapshot": None, "drift_apy": 0.0, "savings_usd": h.get("cumulative_savings_usd", 0.0)}) continue try: - snap = fetch_btcswp_snapshot(profile) + snap = fetch_cfi_funding_snapshot(profile) except Exception: snap = None current_hr = fetch_hl_current_funding_hr(h.get("coin", "BTC")) diff --git a/cli/commands/run.py b/cli/commands/run.py index 30cb779..49895c0 100644 --- a/cli/commands/run.py +++ b/cli/commands/run.py @@ -291,20 +291,4 @@ def _run_anomaly_detector(): if markout_tracker is not None: engine.markout_tracker = markout_tracker - # Attach Guard if configured - if cfg.guard and cfg.guard.get("enabled"): - from modules.guard_config import GuardConfig, PRESETS - - preset_name = cfg.guard.get("preset") - if preset_name and preset_name in PRESETS: - guard_cfg = GuardConfig.from_dict(PRESETS[preset_name].to_dict()) - else: - guard_cfg = GuardConfig.from_dict(cfg.guard) - - if "leverage" in cfg.guard: - guard_cfg.leverage = float(cfg.guard["leverage"]) - - engine.guard_config = guard_cfg - typer.echo(f"Guard: enabled (preset={preset_name or 'custom'}, tiers={len(guard_cfg.tiers)})") - engine.run(max_ticks=cfg.max_ticks, resume=resume) diff --git a/cli/engine.py b/cli/engine.py index 5862685..988d912 100644 --- a/cli/engine.py +++ b/cli/engine.py @@ -65,11 +65,7 @@ def __init__( self._consecutive_timeouts = 0 self._tick_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="tick") - # Optional Guard (composable mode — set via guard_config) - self.guard_bridge = None # type: ignore[assignment] - self.guard_config = None # type: ignore[assignment] - - # Managed order book (brackets, conditionals, pegged orders) +# Managed order book (brackets, conditionals, pegged orders) self.managed_orders = ManagedOrderBook() # Optional markout tracker (measures fill quality vs anomaly state) @@ -272,21 +268,6 @@ def _tick(self) -> None: detector_scores=detector_scores, ) - # 7b. Lazy Guard init (after first fill establishes a position) - if self.guard_config is not None and self.guard_bridge is None and fills: - pos = self.position_tracker.get_agent_position(agent_id, self.instrument) - if pos.net_qty != ZERO: - self._init_guard_bridge(pos) - - # 7c. Sync Guard position size with tracker (handles partial closes / add-ons) - if self.guard_bridge is not None and self.guard_bridge.is_active and fills: - pos = self.position_tracker.get_agent_position(agent_id, self.instrument) - if pos.net_qty == ZERO: - # Position fully closed by strategy — deactivate Guard - self.guard_bridge.mark_closed(snapshot.mid_price, "Position closed by strategy") - else: - self.guard_bridge.state.position_size = float(abs(pos.net_qty)) - # 7d. Update markout windows with current mid price if self.markout_tracker is not None: self.markout_tracker.update(snapshot.mid_price, snapshot.timestamp_ms) @@ -320,105 +301,6 @@ def _tick(self) -> None: # 10. Log tick self._log_tick(snapshot, valid_decisions, fills, ok=True) - # 11. Guard check (composable mode) - if self.guard_bridge is not None and self.guard_bridge.is_active: - from modules.trailing_stop import GuardAction - result = self.guard_bridge.check(snapshot.mid_price) - _CLOSE_ACTIONS = {GuardAction.CLOSE, GuardAction.PHASE1_TIMEOUT, GuardAction.WEAK_PEAK_CUT} - if result.action in _CLOSE_ACTIONS: - _labels = { - GuardAction.CLOSE: "GUARD CLOSE", - GuardAction.PHASE1_TIMEOUT: "PHASE1 TIMEOUT (90min no-graduation)", - GuardAction.WEAK_PEAK_CUT: "WEAK PEAK CUT (45min, peak ROE < 3%)", - } - label = _labels.get(result.action, result.action.value) - elapsed_s = ((time.time() * 1000 - result.state.phase1_start_ts) / 1000 - if result.state.phase1_start_ts else 0) - log.warning("%s: %s | roe=%.2f%% high_water=%.4f elapsed=%.0fs", - label, result.reason, - result.state.current_roe, - result.state.high_water, - elapsed_s) - self._guard_close_position(snapshot) - self.guard_bridge.mark_closed(snapshot.mid_price, result.reason) - self._running = False - - def _guard_close_position(self, snapshot: MarketSnapshot) -> None: - """Close position when Guard trailing stop triggers.""" - agent_id = self.strategy.strategy_id - pos = self.position_tracker.get_agent_position(agent_id, self.instrument) - if pos.net_qty == ZERO: - return - - close_side = "sell" if pos.net_qty > ZERO else "buy" - size = float(abs(pos.net_qty)) - if close_side == "sell": - price = round(float(snapshot.bid) * 0.995, 6) - else: - price = round(float(snapshot.ask) * 1.005, 6) - - if self.dry_run: - log.info("[DRY RUN] Guard close: %s %.6f @ %.4f", close_side, size, price) - return - - fill = self.hl.place_order( - instrument=self.instrument, - side=close_side, - size=size, - price=price, - tif="Ioc", - builder=self.builder, - ) - if fill: - self.position_tracker.apply_fill( - agent_id, self.instrument, fill.side, - fill.quantity, fill.price, - ) - self.trade_log.append({ - "tick": self.tick_count, - "oid": fill.oid, - "instrument": fill.instrument, - "side": fill.side, - "price": str(fill.price), - "quantity": str(fill.quantity), - "timestamp_ms": fill.timestamp_ms, - "fee": str(fill.fee), - "strategy": self.strategy.strategy_id, - "meta": "guard_close", - }) - log.info("Guard closed position: %s %s @ %s", fill.side, fill.quantity, fill.price) - else: - log.warning("Guard close order did not fill — will retry next tick") - self._running = True # Keep running to retry - - def _init_guard_bridge(self, pos) -> None: - """Initialize Guard from guard_config after first position is established.""" - from modules.guard_config import GuardConfig - from modules.guard_bridge import GuardBridge - from modules.guard_state import GuardState - - direction = "long" if pos.net_qty > ZERO else "short" - self.guard_config.direction = direction - - # Auto-compute absolute floor if not set - entry = float(pos.avg_entry_price) - if self.guard_config.phase1_absolute_floor == 0.0: - lev = self.guard_config.leverage - if direction == "long": - self.guard_config.phase1_absolute_floor = entry * (1 - 0.03 / lev) - else: - self.guard_config.phase1_absolute_floor = entry * (1 + 0.03 / lev) - - guard_state = GuardState.new( - instrument=self.instrument, - entry_price=entry, - position_size=float(abs(pos.net_qty)), - direction=direction, - ) - self.guard_bridge = GuardBridge(config=self.guard_config, state=guard_state) - log.info("Guard activated: entry=%.4f size=%.6f dir=%s", - entry, float(abs(pos.net_qty)), direction) - def _close_all_positions(self) -> None: """Close all open positions on shutdown to avoid orphaned exposure.""" agent_id = self.strategy.strategy_id diff --git a/cli/hedge_display.py b/cli/hedge_display.py index 8db8e33..5e9e267 100644 --- a/cli/hedge_display.py +++ b/cli/hedge_display.py @@ -101,7 +101,7 @@ def hedge_proposal_block(proposal, snapshot, *, mainnet: bool = False) -> str: else "↓" if proposal.wire_drift_per_hour_usd < -0.5 else "↔" ) - src = "SEDA live" if snapshot.source == "seda" else f"{YELLOW}replay (HL fundingHistory){RESET}" + src = f"{GREEN}HL fundingHistory{RESET}" network = "mainnet" if mainnet else "testnet" lines = [ diff --git a/cli/main.py b/cli/main.py index a928b7a..314d9d3 100644 --- a/cli/main.py +++ b/cli/main.py @@ -23,12 +23,7 @@ from cli.commands.trade import trade_cmd from cli.commands.account import account_cmd from cli.commands.strategies import strategies_cmd -from cli.commands.guard import guard_app -from cli.commands.radar import radar_app -from cli.commands.pulse import pulse_app -from cli.commands.apex import apex_app from cli.commands.builder import builder_app -from cli.commands.reflect import reflect_app from cli.commands.wallet import wallet_app from cli.commands.setup import setup_app from cli.commands.mcp import mcp_app @@ -54,12 +49,7 @@ app.command("emergency-close", help="Cancel all orders and market-close all positions")(emergency_close_cmd) app.command("order-status", help="Look up a single order by oid")(order_status_cmd) app.command("funding", help="Show current funding rates")(funding_cmd) -app.add_typer(guard_app, name="guard", help="Guard trailing stop system") -app.add_typer(radar_app, name="radar", help="Radar — screen HL perps for setups") -app.add_typer(pulse_app, name="pulse", help="Pulse — detect assets with capital inflow") -app.add_typer(apex_app, name="apex", help="APEX — autonomous multi-slot trading") app.add_typer(builder_app, name="builder", help="Builder fee — revenue collection on trades") -app.add_typer(reflect_app, name="reflect", help="Reflect — performance review and self-improvement") app.add_typer(wallet_app, name="wallet", help="Encrypted keystore wallet management") app.add_typer(setup_app, name="setup", help="Environment validation and setup") app.add_typer(mcp_app, name="mcp", help="MCP server — AI agent tool discovery") diff --git a/cli/mcp_server.py b/cli/mcp_server.py index e360fcf..d2476ad 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -1,7 +1,7 @@ """MCP server for agent-cli — exposes trading tools via Model Context Protocol. Fast tools (account, strategies, builder, wallet, setup) call Python directly. -Long-running tools (run_strategy, apex_run, radar, reflect) use subprocess. +Long-running tools (run_strategy) use subprocess. Every tool carries MCP annotations (readOnlyHint / destructiveHint) so MCP clients can distinguish a harmless read from a fund-moving action. The @@ -27,18 +27,17 @@ # Tools that only read state (no side effects, safe to call freely). _READ_ONLY_TOOLS = { "strategies", "builder_status", "wallet_list", "setup_check", - "account", "status", "apex_status", + "account", "status", "funding_hedge_propose", "funding_hedge_backtest", "agent_memory", "trade_journal", "judge_report", "obsidian_context", "order_status", "funding_rates", } # Tools that move funds or cancel/close live orders/positions — handle with care. _DESTRUCTIVE_TOOLS = { - "trade", "run_strategy", "apex_run", "funding_hedge_execute", + "trade", "run_strategy", "funding_hedge_execute", "schedule_cancel", "emergency_close_all", } -# Everything else (wallet_auto, radar_run, reflect_run) is -# state-changing-but-safe: neither a pure read nor fund-destructive. +# Everything else (wallet_auto) is state-changing-but-safe. _TRUSTED_CONTEXT_SECRET_ENVS = ( "NUNCHI_RUNNER_CONTEXT_SECRET", @@ -357,9 +356,9 @@ def _ann(name: str, title: str): mcp = FastMCP( "yex-trader", instructions=( - "Autonomous Hyperliquid trading CLI — 14 strategies, APEX orchestrator, " - "REFLECT reviews. Always confirm details with the user before calling " - "destructive tools (trade, run_strategy, apex_run, funding_hedge_execute, " + "CFI v2 funding-rate hedge on Hyperliquid — hl hedge propose/execute/status/auto. " + "Always confirm details with the user before calling " + "destructive tools (trade, run_strategy, funding_hedge_execute, " "schedule_cancel, emergency_close_all). " "funding_hedge_execute requires confirmed=true. " "emergency_close_all requires confirm=true." @@ -627,75 +626,6 @@ def run_strategy( env_overrides=env_overrides, ) - @mcp.tool(**_ann("radar_run", "Run radar scan")) - def radar_run(mock: bool = False, ctx: FastMCPContext = None) -> str: - """Run opportunity radar — screen HL perps for trading setups.""" - args = ["radar", "once"] - if mock: - args.append("--mock") - return _run_hl(*args, timeout=60, env_overrides=_request_env(ctx)) - - @mcp.tool(**_ann("apex_status", "APEX status")) - def apex_status(ctx: FastMCPContext = None) -> str: - """Get APEX orchestrator status (slots, positions, daily PnL).""" - return _run_hl("apex", "status", env_overrides=_request_env(ctx)) - - @mcp.tool(**_ann("apex_run", "Run APEX")) - def apex_run( - mock: bool = False, - max_ticks: Optional[int] = None, - preset: str = "default", - mainnet: bool = False, - confirmed: bool = False, - ctx: FastMCPContext = None, - ) -> str: - """Start APEX multi-slot orchestrator. WARNING: places real orders unless mock. - - Args: - mock: Use mock data - max_ticks: Stop after N ticks - preset: Strategy preset (default, conservative, aggressive) - mainnet: Use mainnet - confirmed: Explicit confirmation for hosted gateway sessions that require it. - """ - env_overrides = _request_env(ctx) - effective_max_ticks = max_ticks if max_ticks is not None else _trusted_max_ticks(env_overrides) - error = _context_limit_error( - "apex_run", - env_overrides, - mainnet=mainnet, - max_ticks=effective_max_ticks, - confirmed=confirmed, - require_signing=not mock, - ) - if error: - return _json_error(error) - - args = ["apex", "run", "--preset", preset] - if mock: - args.append("--mock") - if effective_max_ticks is not None: - args.extend(["--max-ticks", str(effective_max_ticks)]) - if mainnet: - args.append("--mainnet") - return _run_hl( - *args, - timeout=max(120, (effective_max_ticks or 10) * 60 + 30), - env_overrides=env_overrides, - ) - - @mcp.tool(**_ann("reflect_run", "Run reflect review")) - def reflect_run(since: Optional[str] = None, ctx: FastMCPContext = None) -> str: - """Run REFLECT performance review — analyze trades and generate report. - - Args: - since: Start date for analysis (YYYY-MM-DD). Default: since last report. - """ - args = ["reflect", "run"] - if since: - args.extend(["--since", since]) - return _run_hl(*args, env_overrides=_request_env(ctx)) - # ------------------------------------------------------------------ # Safety tools — dead-man's switch + panic close # ------------------------------------------------------------------ diff --git a/pyproject.toml b/pyproject.toml index f567620..97a45fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ Repository = "https://github.com/Nunchi-trade/agent-cli" [tool.pytest.ini_options] pythonpath = ["."] -norecursedirs = ["_archive", ".*", "build", "dist"] +norecursedirs = ["_archive", "archive", ".*", "build", "dist"] [tool.mypy] python_version = "3.10" @@ -56,4 +56,4 @@ disallow_untyped_defs = false ignore_missing_imports = true [tool.setuptools.packages.find] -include = ["cli*", "strategies*", "sdk*", "common*", "parent*", "modules*", "skills*", "quoting_engine*"] +include = ["cli*", "strategies*", "sdk*", "common*", "parent*", "modules*", "skills*"] diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index 733309a..a500158 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -35,9 +35,6 @@ "setup_check", "account", "status", - "apex_status", - "radar_run", - "reflect_run", "agent_memory", "trade_journal", "judge_report", @@ -51,7 +48,6 @@ "wallet_auto", "trade", "run_strategy", - "apex_run", "funding_hedge_execute", "schedule_cancel", "emergency_close_all", @@ -79,7 +75,7 @@ def do_GET(self): elif self.path == "/status": try: result = subprocess.run( - [sys.executable, "-m", "cli.main", "apex", "status"], + [sys.executable, "-m", "cli.main", "status"], capture_output=True, text=True, timeout=10, ) output = result.stdout.strip() or result.stderr.strip() or "(no output)" @@ -306,41 +302,10 @@ def build_command() -> list[str]: py = [sys.executable, "-m", "cli.main"] if mode in ("apex", "wolf"): - cmd = py + ["apex", "run"] - preset = os.environ.get("APEX_PRESET") - if preset: - cmd += ["--preset", preset] - budget = os.environ.get("APEX_BUDGET") - if budget: - cmd += ["--budget", budget] - slots = os.environ.get("APEX_SLOTS") - if slots: - cmd += ["--slots", slots] - leverage = os.environ.get("APEX_LEVERAGE") - if leverage: - cmd += ["--leverage", leverage] - tick = os.environ.get("TICK_INTERVAL") - if tick: - cmd += ["--tick", tick] - # Restrict the agent's pulse/radar scans and entries to a set of - # markets. Critical for PR-3 dedicated-wallet mode where the agent - # is funded on a HIP-3 dex (e.g. yex) and must NOT scan universal - # HL perps that it has no collateral on. Without this, agents - # scan 207+ universal markets and produce zero entries even though - # they hold $1000 in their yex clearinghouse. - allowed = os.environ.get("ALLOWED_INSTRUMENTS") - if allowed: - cmd += ["--markets", allowed] - strategy_names = os.environ.get("STRATEGY_NAMES") - if strategy_names: - cmd += ["--strategy-names", strategy_names] - base_dir = os.environ.get("DATA_DIR", "/data") - cmd += ["--data-dir", f"{base_dir}/apex"] - if os.environ.get("HL_TESTNET", "true").lower() == "false": - cmd.append("--mainnet") - return cmd + log.warning("RUN_MODE=%s is deprecated; use strategy or mcp. Falling back to cfi_hedge.", mode) + mode = "strategy" - elif mode == "strategy": + if mode == "strategy": strategy = os.environ.get("STRATEGY", "cfi_hedge") instrument = os.environ.get("INSTRUMENT", "ETH-PERP") tick = os.environ.get("TICK_INTERVAL", "10") @@ -353,7 +318,7 @@ def build_command() -> list[str]: return py + ["mcp", "serve", "--transport", "sse"] else: - log.error("Unknown RUN_MODE: %s. Use apex, wolf, strategy, or mcp.", mode) + log.error("Unknown RUN_MODE: %s. Use strategy or mcp.", mode) sys.exit(1) @@ -424,18 +389,7 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: if name == "status": return _run_hl("status", env_overrides=env_overrides) if name == "apex_status": - return _run_hl("apex", "status", env_overrides=env_overrides) - if name == "radar_run": - cmd = ["radar", "once"] - if _bool_arg(arguments, "mock"): - cmd.append("--mock") - return _run_hl(*cmd, timeout=60, env_overrides=env_overrides) - if name == "reflect_run": - cmd = ["reflect", "run"] - since = _str_arg(arguments, "since") - if since: - cmd.extend(["--since", since]) - return _run_hl(*cmd, env_overrides=env_overrides) + return _run_hl("status", env_overrides=env_overrides) if name == "order_status": oid = _str_arg(arguments, "oid") if not oid: @@ -537,32 +491,6 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: cmd.append("--mainnet") return _run_hl(*cmd, timeout=max(60, (effective_max_ticks or 10) * tick + 30), env_overrides=env_overrides) - if name == "apex_run": - mock = _bool_arg(arguments, "mock") - max_ticks = _int_arg(arguments, "max_ticks") - preset = _str_arg(arguments, "preset") or "default" - mainnet = _bool_arg(arguments, "mainnet") - confirmed = _bool_arg(arguments, "confirmed") - effective_max_ticks = max_ticks if max_ticks is not None else _trusted_max_ticks(env_overrides) - error = _context_limit_error( - "apex_run", - env_overrides, - mainnet=mainnet, - max_ticks=effective_max_ticks, - confirmed=confirmed, - require_signing=not mock, - ) - if error: - return _json_error(error) - cmd = ["apex", "run", "--preset", preset] - if mock: - cmd.append("--mock") - if effective_max_ticks is not None: - cmd.extend(["--max-ticks", str(effective_max_ticks)]) - if mainnet: - cmd.append("--mainnet") - return _run_hl(*cmd, timeout=max(120, (effective_max_ticks or 10) * 60 + 30), env_overrides=env_overrides) - if name == "funding_hedge_execute": coin = _str_arg(arguments, "coin") or "BTC" dry_run = _bool_arg(arguments, "dry_run") diff --git a/scripts/validate_agent_cli.py b/scripts/validate_agent_cli.py index b08ca45..704708b 100644 --- a/scripts/validate_agent_cli.py +++ b/scripts/validate_agent_cli.py @@ -130,7 +130,6 @@ def base_cases(python: str, quick_pytest: bool) -> list[TestCase]: "strategies", "modules", "skills", - "quoting_engine", "scripts", ], python, @@ -157,11 +156,9 @@ def base_cases(python: str, quick_pytest: bool) -> list[TestCase]: "setup", "wallet", "builder", - "radar", - "pulse", - "apex", - "guard", - "reflect", + "hedge", + "margin", + "trading", "journal", "keys", "mcp", @@ -181,7 +178,7 @@ def base_cases(python: str, quick_pytest: bool) -> list[TestCase]: def node_cases() -> list[TestCase]: cases: list[TestCase] = [] - for path in sorted((REPO_ROOT / "deploy").glob("*/src/*")): + for path in sorted((REPO_ROOT / "_archive" / "deploy").glob("*/src/*")): if path.suffix not in {".js", ".mjs"}: continue cases.append( @@ -259,67 +256,6 @@ def e2e_cases(python: str, data_root: Path) -> list[TestCase]: timeout_s=60, profiles=("e2e", "production"), ), - TestCase( - name="radar_once_mock", - stage="e2e_mock", - command=python_cmd( - [ - "-m", - "cli.main", - "radar", - "once", - "--mock", - "--top-n", - "3", - "--min-volume", - "0", - "--score-threshold", - "0", - "--data-dir", - str(data_root / "radar"), - ], - python, - ), - timeout_s=60, - profiles=("e2e", "production"), - ), - TestCase( - name="pulse_once_mock", - stage="e2e_mock", - command=python_cmd( - [ - "-m", - "cli.main", - "pulse", - "once", - "--mock", - "--min-volume", - "0", - "--data-dir", - str(data_root / "pulse"), - ], - python, - ), - timeout_s=60, - profiles=("e2e", "production"), - ), - TestCase( - name="apex_once_mock", - stage="e2e_mock", - command=python_cmd( - ["-m", "cli.main", "apex", "once", "--mock", "--data-dir", str(data_root / "apex")], - python, - ), - timeout_s=90, - profiles=("e2e", "production"), - ), - TestCase( - name="apex_status_after_mock", - stage="e2e_mock", - command=python_cmd(["-m", "cli.main", "apex", "status", "--data-dir", str(data_root / "apex")], python), - timeout_s=30, - profiles=("e2e", "production"), - ), TestCase( name="status_reader_json_contracts", stage="api_contract", @@ -350,7 +286,7 @@ def status_reader_contract_code(data_root: Path) -> str: return ( "import json, subprocess, sys; " f"data_dir={data}; " - "cmds=['status','strategies','trades','reflect','radar','journal']; " + "cmds=['status','strategies','trades','journal']; " "seen={}; " "\nfor cmd in cmds:\n" " args=[sys.executable,'-m','cli.api.status_reader',cmd,'--data-dir',data_dir]\n" @@ -432,29 +368,6 @@ def production_cases(python: str, mainnet: bool, live_data_root: Path) -> list[T profiles=("production",), live_probe=True, ), - TestCase( - name="live_radar_readonly_scan", - stage="production_readonly", - command=python_cmd( - [ - "-m", - "cli.main", - "radar", - "once", - *network_flag, - "--top-n", - "5", - "--score-threshold", - "9999", - "--data-dir", - str(live_data_root / "radar"), - ], - python, - ), - timeout_s=90, - profiles=("production",), - live_probe=True, - ), ] diff --git a/strategies/cfi_funding.py b/strategies/cfi_funding.py new file mode 100644 index 0000000..f627be8 --- /dev/null +++ b/strategies/cfi_funding.py @@ -0,0 +1,146 @@ +"""CFI v2 funding inputs from Hyperliquid only. + +No SEDA/oracle dependency — hedge math stays in `strategies/cfi_hedge.py`. +K2 fixed leg and r_ema are replayed from HL `fundingHistory`; current funding +from `metaAndAssetCtxs`. +""" +from __future__ import annotations + +import logging +import os +import time +from dataclasses import dataclass +from typing import Literal, Optional + +import requests + +from strategies.cfi_hedge import ( + BTCSWP_PROFILE, + CFIAssetProfile, + FundingRateSample, + compute_k2_from_history, +) + +log = logging.getLogger(__name__) + +HL_INFO_URL_DEFAULT = "https://api.hyperliquid.xyz/info" + + +def _hl_info_url() -> str: + return os.environ.get("HL_INFO_URL", HL_INFO_URL_DEFAULT) + + +@dataclass(frozen=True) +class CfiFundingSnapshot: + """K2 + r_ema computed from HL funding history.""" + + source: Literal["hl"] + timestamp_iso: str + timestamp_ms: int + oracle_px: Optional[float] + k_fixed_hr: float + r_ema_hr: float + cfi: Optional[float] + + +def _fetch_funding_history( + coin: str, + lookback_hours: int, + *, + timeout_s: float = 10.0, +) -> list[dict]: + """Pull HL `fundingHistory` for a coin over the lookback window.""" + end_time = int(time.time() * 1000) + start_time = end_time - lookback_hours * 60 * 60 * 1000 + resp = requests.post( + _hl_info_url(), + json={ + "type": "fundingHistory", + "coin": coin, + "startTime": start_time, + "endTime": end_time, + }, + timeout=timeout_s, + ) + resp.raise_for_status() + return resp.json() or [] + + +def fetch_hl_current_funding_hr( + coin: str, + *, + timeout_s: float = 10.0, +) -> Optional[float]: + """Predicted current hourly funding rate for an HL coin.""" + try: + resp = requests.post( + _hl_info_url(), + json={"type": "metaAndAssetCtxs"}, + timeout=timeout_s, + ) + resp.raise_for_status() + data = resp.json() + universe = data[0]["universe"] + ctxs = data[1] + for meta, ctx in zip(universe, ctxs): + if meta.get("name") == coin: + return float(ctx.get("funding", 0)) + return None + except Exception as e: + log.warning("cfi_funding: fetch_hl_current_funding_hr(%s) failed: %s", coin, e) + return None + + +def replay_k2_from_hl( + profile: CFIAssetProfile = BTCSWP_PROFILE, + *, + lookback_hours: int = 168, + timeout_s: float = 10.0, +) -> CfiFundingSnapshot: + """Compute K2 + r_ema locally from HL `fundingHistory`.""" + hist = _fetch_funding_history( + profile.hl_coin, + lookback_hours, + timeout_s=timeout_s, + ) + samples = [ + FundingRateSample( + funding_rate=float(e["fundingRate"]), + time=int(e["time"]), + ) + for e in hist + ] + k = compute_k2_from_history(samples, profile) + + if samples: + r_ema = samples[0].funding_rate + for s in samples: + r_ema = (1.0 - profile.k2_beta) * r_ema + profile.k2_beta * s.funding_rate + else: + r_ema = profile.fixed_leg_initial + + now_ms = int(time.time() * 1000) + now_iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now_ms / 1000)) + return CfiFundingSnapshot( + source="hl", + timestamp_iso=now_iso, + timestamp_ms=now_ms, + oracle_px=None, + k_fixed_hr=k, + r_ema_hr=r_ema, + cfi=None, + ) + + +def fetch_cfi_funding_snapshot( + profile: CFIAssetProfile = BTCSWP_PROFILE, + *, + timeout_s: float = 10.0, +) -> CfiFundingSnapshot: + """Live K2 inputs from HL funding history only.""" + return replay_k2_from_hl(profile, timeout_s=timeout_s) + + +# Back-compat aliases for callers migrating off seda_oracle naming. +BTCSWPOracleSnapshot = CfiFundingSnapshot +fetch_btcswp_snapshot = fetch_cfi_funding_snapshot diff --git a/strategies/cfi_hedge.py b/strategies/cfi_hedge.py index 1e42e3c..2b050b6 100644 --- a/strategies/cfi_hedge.py +++ b/strategies/cfi_hedge.py @@ -140,7 +140,7 @@ def apy_to_hourly(apy: float) -> float: return apy / HOURS_PER_YEAR -# ─── K2 EMA replay (fallback when SEDA oracle is unreachable) ─────────────── +# ─── K2 EMA replay from HL fundingHistory ─────────────────────────────────── @dataclass(frozen=True) diff --git a/tests/_archive/README.md b/tests/_archive/README.md index 91a9979..d9b31b4 100644 --- a/tests/_archive/README.md +++ b/tests/_archive/README.md @@ -1,6 +1,11 @@ -# Archived strategy tests +# Archived strategy and quoting-engine tests -Tests for legacy strategies archived on **2026-07-02**. Not collected by default pytest runs +Tests for legacy strategies and the quoting engine archived on **2026-07-02**. Not collected by default pytest runs (`norecursedirs` excludes `_archive`). -Run manually if needed: `pytest tests/_archive/ -v` +Run manually if needed: + +```bash +pytest tests/_archive/ -v +PYTHONPATH=_archive:. pytest tests/_archive/quoting_engine/ -v +``` diff --git a/tests/quoting_engine/conftest.py b/tests/_archive/quoting_engine/conftest.py similarity index 78% rename from tests/quoting_engine/conftest.py rename to tests/_archive/quoting_engine/conftest.py index 4339da0..0fe18ea 100644 --- a/tests/quoting_engine/conftest.py +++ b/tests/_archive/quoting_engine/conftest.py @@ -1,5 +1,14 @@ """Shared fixtures for quoting engine tests.""" +import sys +from pathlib import Path + import pytest + +# Archived package lives under repo-root/_archive/quoting_engine. +_archive_root = Path(__file__).resolve().parents[2] / "_archive" +if str(_archive_root) not in sys.path: + sys.path.insert(0, str(_archive_root)) + from quoting_engine.config import ( MarketConfig, FairValueWeights, SpreadParams, LadderParams, SkewParams, diff --git a/tests/quoting_engine/test_qe_config.py b/tests/_archive/quoting_engine/test_qe_config.py similarity index 100% rename from tests/quoting_engine/test_qe_config.py rename to tests/_archive/quoting_engine/test_qe_config.py diff --git a/tests/quoting_engine/test_qe_disagreement.py b/tests/_archive/quoting_engine/test_qe_disagreement.py similarity index 100% rename from tests/quoting_engine/test_qe_disagreement.py rename to tests/_archive/quoting_engine/test_qe_disagreement.py diff --git a/tests/quoting_engine/test_qe_engine.py b/tests/_archive/quoting_engine/test_qe_engine.py similarity index 100% rename from tests/quoting_engine/test_qe_engine.py rename to tests/_archive/quoting_engine/test_qe_engine.py diff --git a/tests/quoting_engine/test_qe_event_schedule_calendar.py b/tests/_archive/quoting_engine/test_qe_event_schedule_calendar.py similarity index 100% rename from tests/quoting_engine/test_qe_event_schedule_calendar.py rename to tests/_archive/quoting_engine/test_qe_event_schedule_calendar.py diff --git a/tests/quoting_engine/test_qe_fair_value.py b/tests/_archive/quoting_engine/test_qe_fair_value.py similarity index 100% rename from tests/quoting_engine/test_qe_fair_value.py rename to tests/_archive/quoting_engine/test_qe_fair_value.py diff --git a/tests/quoting_engine/test_qe_feeds_base.py b/tests/_archive/quoting_engine/test_qe_feeds_base.py similarity index 100% rename from tests/quoting_engine/test_qe_feeds_base.py rename to tests/_archive/quoting_engine/test_qe_feeds_base.py diff --git a/tests/quoting_engine/test_qe_funding_boundary.py b/tests/_archive/quoting_engine/test_qe_funding_boundary.py similarity index 100% rename from tests/quoting_engine/test_qe_funding_boundary.py rename to tests/_archive/quoting_engine/test_qe_funding_boundary.py diff --git a/tests/quoting_engine/test_qe_funding_rate_feed.py b/tests/_archive/quoting_engine/test_qe_funding_rate_feed.py similarity index 100% rename from tests/quoting_engine/test_qe_funding_rate_feed.py rename to tests/_archive/quoting_engine/test_qe_funding_rate_feed.py diff --git a/tests/quoting_engine/test_qe_fv_band.py b/tests/_archive/quoting_engine/test_qe_fv_band.py similarity index 100% rename from tests/quoting_engine/test_qe_fv_band.py rename to tests/_archive/quoting_engine/test_qe_fv_band.py diff --git a/tests/quoting_engine/test_qe_inventory.py b/tests/_archive/quoting_engine/test_qe_inventory.py similarity index 100% rename from tests/quoting_engine/test_qe_inventory.py rename to tests/_archive/quoting_engine/test_qe_inventory.py diff --git a/tests/quoting_engine/test_qe_inventory_caps.py b/tests/_archive/quoting_engine/test_qe_inventory_caps.py similarity index 100% rename from tests/quoting_engine/test_qe_inventory_caps.py rename to tests/_archive/quoting_engine/test_qe_inventory_caps.py diff --git a/tests/quoting_engine/test_qe_ladder.py b/tests/_archive/quoting_engine/test_qe_ladder.py similarity index 100% rename from tests/quoting_engine/test_qe_ladder.py rename to tests/_archive/quoting_engine/test_qe_ladder.py diff --git a/tests/quoting_engine/test_qe_liq_advanced.py b/tests/_archive/quoting_engine/test_qe_liq_advanced.py similarity index 100% rename from tests/quoting_engine/test_qe_liq_advanced.py rename to tests/_archive/quoting_engine/test_qe_liq_advanced.py diff --git a/tests/quoting_engine/test_qe_metrics.py b/tests/_archive/quoting_engine/test_qe_metrics.py similarity index 100% rename from tests/quoting_engine/test_qe_metrics.py rename to tests/_archive/quoting_engine/test_qe_metrics.py diff --git a/tests/quoting_engine/test_qe_microprice.py b/tests/_archive/quoting_engine/test_qe_microprice.py similarity index 100% rename from tests/quoting_engine/test_qe_microprice.py rename to tests/_archive/quoting_engine/test_qe_microprice.py diff --git a/tests/quoting_engine/test_qe_oracle_monitor.py b/tests/_archive/quoting_engine/test_qe_oracle_monitor.py similarity index 100% rename from tests/quoting_engine/test_qe_oracle_monitor.py rename to tests/_archive/quoting_engine/test_qe_oracle_monitor.py diff --git a/tests/quoting_engine/test_qe_regime.py b/tests/_archive/quoting_engine/test_qe_regime.py similarity index 100% rename from tests/quoting_engine/test_qe_regime.py rename to tests/_archive/quoting_engine/test_qe_regime.py diff --git a/tests/quoting_engine/test_qe_spread.py b/tests/_archive/quoting_engine/test_qe_spread.py similarity index 100% rename from tests/quoting_engine/test_qe_spread.py rename to tests/_archive/quoting_engine/test_qe_spread.py diff --git a/tests/quoting_engine/test_qe_toxicity.py b/tests/_archive/quoting_engine/test_qe_toxicity.py similarity index 100% rename from tests/quoting_engine/test_qe_toxicity.py rename to tests/_archive/quoting_engine/test_qe_toxicity.py diff --git a/tests/quoting_engine/test_qe_toxicity_tiered.py b/tests/_archive/quoting_engine/test_qe_toxicity_tiered.py similarity index 100% rename from tests/quoting_engine/test_qe_toxicity_tiered.py rename to tests/_archive/quoting_engine/test_qe_toxicity_tiered.py diff --git a/tests/quoting_engine/test_qe_vol_estimator.py b/tests/_archive/quoting_engine/test_qe_vol_estimator.py similarity index 100% rename from tests/quoting_engine/test_qe_vol_estimator.py rename to tests/_archive/quoting_engine/test_qe_vol_estimator.py diff --git a/tests/test_builder_fee.py b/tests/test_builder_fee.py index d001a39..8812616 100644 --- a/tests/test_builder_fee.py +++ b/tests/test_builder_fee.py @@ -170,23 +170,3 @@ def test_no_builder_when_none(self): call_kwargs = mock_hl.place_order.call_args assert call_kwargs.kwargs.get("builder") is None - -# --------------------------------------------------------------------------- -# ApexRunner accepts builder -# --------------------------------------------------------------------------- - -class TestApexRunnerBuilder: - def test_apex_runner_stores_builder(self): - from skills.apex.scripts.standalone_runner import ApexRunner - - mock_hl = MagicMock() - builder_info = {"b": "0xAPEX", "f": 10} - runner = ApexRunner(hl=mock_hl, builder=builder_info) - assert runner.builder == builder_info - - def test_apex_runner_builder_default_none(self): - from skills.apex.scripts.standalone_runner import ApexRunner - - mock_hl = MagicMock() - runner = ApexRunner(hl=mock_hl) - assert runner.builder is None diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 1456700..6206066 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -27,56 +27,22 @@ def test_default_mode_is_mcp(self, monkeypatch): cmd = build_command() assert cmd == [sys.executable, "-m", "cli.main", "mcp", "serve", "--transport", "sse"] - def test_apex_mode_default(self, monkeypatch): + def test_apex_mode_falls_back_to_strategy(self, monkeypatch): monkeypatch.setenv("RUN_MODE", "apex") - monkeypatch.delenv("APEX_PRESET", raising=False) - monkeypatch.delenv("APEX_BUDGET", raising=False) - monkeypatch.delenv("APEX_SLOTS", raising=False) - monkeypatch.delenv("APEX_LEVERAGE", raising=False) - monkeypatch.delenv("TICK_INTERVAL", raising=False) + monkeypatch.setenv("STRATEGY", "cfi_hedge") + monkeypatch.setenv("INSTRUMENT", "BTC-PERP") + monkeypatch.setenv("TICK_INTERVAL", "10") monkeypatch.setenv("HL_TESTNET", "true") cmd = build_command() - assert cmd[:3] == [sys.executable, "-m", "cli.main"] - assert "apex" in cmd - assert "run" in cmd - assert "--data-dir" in cmd - assert "--mainnet" not in cmd - - def test_apex_mode_with_all_options(self, monkeypatch): - monkeypatch.setenv("RUN_MODE", "apex") - monkeypatch.setenv("APEX_PRESET", "aggressive") - monkeypatch.setenv("APEX_BUDGET", "1000") - monkeypatch.setenv("APEX_SLOTS", "5") - monkeypatch.setenv("APEX_LEVERAGE", "10") - monkeypatch.setenv("TICK_INTERVAL", "30") - monkeypatch.setenv("HL_TESTNET", "false") - - cmd = build_command() - assert "--preset" in cmd - assert "aggressive" in cmd - assert "--budget" in cmd - assert "1000" in cmd - assert "--slots" in cmd - assert "5" in cmd - assert "--leverage" in cmd - assert "10" in cmd - assert "--tick" in cmd - assert "30" in cmd - assert "--mainnet" in cmd + assert "run" in cmd and "cfi_hedge" in cmd and "apex" not in cmd - def test_wolf_mode(self, monkeypatch): + def test_wolf_mode_falls_back_to_strategy(self, monkeypatch): monkeypatch.setenv("RUN_MODE", "wolf") + monkeypatch.setenv("STRATEGY", "cfi_hedge") monkeypatch.setenv("HL_TESTNET", "true") - monkeypatch.delenv("APEX_PRESET", raising=False) - monkeypatch.delenv("APEX_BUDGET", raising=False) - monkeypatch.delenv("APEX_SLOTS", raising=False) - monkeypatch.delenv("APEX_LEVERAGE", raising=False) - monkeypatch.delenv("TICK_INTERVAL", raising=False) - cmd = build_command() - assert "apex" in cmd - assert "run" in cmd + assert "run" in cmd and "cfi_hedge" in cmd and "apex" not in cmd def test_strategy_mode(self, monkeypatch): monkeypatch.setenv("RUN_MODE", "strategy") diff --git a/tests/test_mcp_annotations.py b/tests/test_mcp_annotations.py index bf868c6..5d3ff5c 100644 --- a/tests/test_mcp_annotations.py +++ b/tests/test_mcp_annotations.py @@ -16,7 +16,6 @@ def test_destructive_set_covers_fund_movers(): for name in ( "trade", "run_strategy", - "apex_run", "funding_hedge_execute", "schedule_cancel", "emergency_close_all", From 2ab6541ade4d00635939a26434146d45298736bc Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Fri, 3 Jul 2026 13:33:02 -0400 Subject: [PATCH 4/5] Harden hosted MCP runner web-auth flows. (#74) Co-authored-by: Cursor --- cli/commands/hedge.py | 112 ++++++++-- cli/mcp_server.py | 11 + scripts/entrypoint.py | 22 +- scripts/mcp_workload_experiment.py | 314 +++++++++++++++++++++++++++++ tests/test_mcp_gateway_context.py | 38 ++++ 5 files changed, 479 insertions(+), 18 deletions(-) create mode 100644 scripts/mcp_workload_experiment.py diff --git a/cli/commands/hedge.py b/cli/commands/hedge.py index 9f83dd7..e3136cf 100644 --- a/cli/commands/hedge.py +++ b/cli/commands/hedge.py @@ -115,6 +115,23 @@ def _build_proposal( Returns (proposal, snapshot) or raises typer.Exit if no position open. """ + state = hl.get_account_state() + return _build_proposal_from_state( + state, + coin, + mainnet=mainnet, + hedge_instrument=hedge_instrument, + ) + + +def _build_proposal_from_state( + state: Optional[dict], + coin: str, + *, + mainnet: bool = False, + hedge_instrument: Optional[str] = None, +): + """Build a hedge proposal from an already-fetched account state.""" from strategies.cfi_hedge import build_cfi_hedge_proposal, get_cfi_profile from strategies.cfi_funding import ( fetch_cfi_funding_snapshot, @@ -130,7 +147,6 @@ def _build_proposal( typer.echo(f"Error: no deployed CFI v2 profile for coin '{coin}'", err=True) raise typer.Exit(2) - state = hl.get_account_state() if not state: typer.echo("Error: could not fetch HL account state", err=True) raise typer.Exit(1) @@ -162,6 +178,24 @@ def _build_proposal( return proposal, snapshot +def _build_view_only_proposal( + address: str, + coin: str, + *, + mainnet: bool = False, + hedge_instrument: Optional[str] = None, +): + from cli.hl_adapter import read_only_account_state + + state = read_only_account_state(address, testnet=not mainnet) + return _build_proposal_from_state( + state, + coin, + mainnet=mainnet, + hedge_instrument=hedge_instrument, + ) + + # ─── propose ───────────────────────────────────────────────────────────────── @@ -169,6 +203,12 @@ def _build_proposal( def propose_cmd( coin: str = typer.Argument("BTC", help="Coin to hedge (BTC, ETH)"), mainnet: bool = typer.Option(False, "--mainnet", help="Use mainnet (default: testnet)"), + address: Optional[str] = typer.Option( + None, + "--address", + "-a", + help="View-only: build proposal for this address without loading a signing key.", + ), ): """Show a CFI v2 hedge proposal without executing.""" _boot_cli() @@ -176,14 +216,18 @@ def propose_cmd( from cli.config import TradingConfig from cli.hedge_display import hedge_proposal_block from cli.hl_adapter import DirectHLProxy + from cli.view_mode import view_address from parent.hl_proxy import HLProxy - cfg = TradingConfig() - private_key = cfg.get_private_key() - raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) - hl = DirectHLProxy(raw_hl) - - proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) + view_only_address = view_address(address) + if view_only_address: + proposal, snapshot = _build_view_only_proposal(view_only_address, coin, mainnet=mainnet) + else: + cfg = TradingConfig() + private_key = cfg.get_private_key() + raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) + hl = DirectHLProxy(raw_hl) + proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) typer.echo(hedge_proposal_block(proposal, snapshot, mainnet=mainnet)) @@ -196,6 +240,12 @@ def execute_cmd( dry_run: bool = typer.Option(False, "--dry-run", help="Preview only; do not sign or submit"), yes: bool = typer.Option(False, "--yes", "-y", help="Skip interactive confirm"), mainnet: bool = typer.Option(False, "--mainnet", help="Use mainnet (default: testnet)"), + address: Optional[str] = typer.Option( + None, + "--address", + "-a", + help="View-only dry-run: preview this address without loading a signing key.", + ), ): """Build the proposal and optionally sign + submit a real yex:{COIN}SWP order. @@ -207,14 +257,19 @@ def execute_cmd( from cli.display import BOLD, GREEN, RESET from cli.hedge_display import hedge_proposal_block from cli.hl_adapter import DirectHLProxy + from cli.view_mode import view_address from parent.hl_proxy import HLProxy - cfg = TradingConfig() - private_key = cfg.get_private_key() - raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) - hl = DirectHLProxy(raw_hl) - - proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) + view_only_address = view_address(address) + if view_only_address and dry_run: + proposal, snapshot = _build_view_only_proposal(view_only_address, coin, mainnet=mainnet) + hl = None + else: + cfg = TradingConfig() + private_key = cfg.get_private_key() + raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) + hl = DirectHLProxy(raw_hl) + proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) typer.echo(hedge_proposal_block(proposal, snapshot, mainnet=mainnet)) # Size the order in CFI v2 (BTCSWP) units. SDK rounds to szDecimals. @@ -417,12 +472,35 @@ def backtest_cmd( / "hedge_calculator.py" ) if not script_path.exists(): + from strategies.cfi_hedge import get_cfi_profile, hourly_to_apy + from strategies.cfi_funding import fetch_cfi_funding_snapshot + + profile = get_cfi_profile(coin, mainnet=False) + if profile is None: + typer.echo(f"Error: no CFI profile for coin '{coin}'", err=True) + raise typer.Exit(2) + snapshot = fetch_cfi_funding_snapshot(profile) + hedge_notional = notional / profile.vol_mult_l + fixed_cost = notional * snapshot.k_fixed_hr * 24 * days typer.echo( - f"Error: hedge_calculator.py not found at {script_path}. " - f"Pass --script to override.", - err=True, + json.dumps( + { + "mode": "builtin_cfi_projection", + "note": "Reference hedge_calculator.py was not packaged; using built-in CFI v2 math.", + "coin": coin.upper(), + "days": days, + "perp_notional_usd": notional, + "hedge_notional_usd": hedge_notional, + "vol_mult_l": profile.vol_mult_l, + "k_fixed_hr": snapshot.k_fixed_hr, + "k_fixed_apy": hourly_to_apy(snapshot.k_fixed_hr), + "estimated_fixed_leg_cost_usd": fixed_cost, + "script_path_missing": str(script_path), + }, + indent=2, + ) ) - raise typer.Exit(2) + return cmd = [ sys.executable, diff --git a/cli/mcp_server.py b/cli/mcp_server.py index d2476ad..f55d5d5 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -54,6 +54,8 @@ "x-nunchi-secret-nunchi-web-auth-pair-token": "NUNCHI_WEB_AUTH_PAIR_TOKEN", "x-nunchi-web-auth-address": "NUNCHI_WEB_AUTH_ADDRESS", "x-nunchi-secret-nunchi-web-auth-address": "NUNCHI_WEB_AUTH_ADDRESS", + "x-nunchi-account-id": "NUNCHI_ACCOUNT_ID", + "x-nunchi-secret-nunchi-account-id": "NUNCHI_ACCOUNT_ID", "x-nunchi-trading-permission-tier": "NUNCHI_TRADING_PERMISSION_TIER", "x-nunchi-secret-nunchi-trading-permission-tier": "NUNCHI_TRADING_PERMISSION_TIER", "x-nunchi-trading-network": "NUNCHI_TRADING_NETWORK", @@ -227,6 +229,11 @@ def _trusted_context_env_overrides(ctx: Any) -> dict[str, str]: policy = _policy_from_context_env(overrides) if policy is not None: overrides["NUNCHI_SESSION_POLICY"] = policy + view_address = _clean_context_value( + overrides.get("NUNCHI_WEB_AUTH_ADDRESS") or overrides.get("NUNCHI_ACCOUNT_ID") + ) + if view_address and "HL_VIEW_AS_USER" not in overrides: + overrides["HL_VIEW_AS_USER"] = view_address return overrides @@ -487,11 +494,15 @@ def setup_check(ctx: FastMCPContext = None) -> str: has_web_auth = bool(env_overrides.get("NUNCHI_WEB_AUTH_PAIR_TOKEN")) and bool( env_overrides.get("NUNCHI_WEB_AUTH_ADDRESS") ) + has_view_only = bool(env_overrides.get("HL_VIEW_AS_USER")) + permission_tier = str(env_overrides.get("NUNCHI_TRADING_PERMISSION_TIER") or "").strip().lower() keystores = list_keystores() if has_env_key: ok_items.append("HL_PRIVATE_KEY set") elif has_web_auth: ok_items.append("web-auth pairing context provided") + elif has_view_only and permission_tier == "read_only": + ok_items.append(f"view-only context provided ({env_overrides.get('HL_VIEW_AS_USER')})") elif keystores: ok_items.append(f"Keystore found ({len(keystores)} keys)") else: diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index a500158..1538bca 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -355,7 +355,7 @@ def handle_mcp_json_rpc(raw_body: bytes, headers: Any) -> tuple[int, dict[str, A if method == "tools/call": name = str(params.get("name", "")) arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {} - return 200, _json_rpc_result(request_id, {"content": [{"type": "text", "text": call_mcp_tool(name, arguments, headers)}]}) + return 200, _json_rpc_result(request_id, _mcp_tool_result(call_mcp_tool(name, arguments, headers))) return 200, _json_rpc_error(request_id, -32601, f"method not found: {method}") except Exception as exc: log.exception("MCP JSON-RPC error") @@ -522,6 +522,22 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: return _json_error(f"unknown tool: {name}") +def _mcp_tool_result(text: str) -> dict[str, Any]: + result: dict[str, Any] = {"content": [{"type": "text", "text": text}]} + try: + parsed = json.loads(text) + except (TypeError, json.JSONDecodeError): + return result + if isinstance(parsed, dict) and parsed.get("error"): + result["isError"] = True + result["structuredContent"] = { + "ok": False, + "error": parsed.get("error"), + **({"code": parsed.get("code")} if parsed.get("code") else {}), + } + return result + + def _context_from_headers(headers: Any) -> Any: normalized = {str(key).lower(): str(value) for key, value in headers.items()} request = SimpleNamespace(headers=normalized) @@ -542,11 +558,15 @@ def _setup_check_text(env_overrides: dict[str, str]) -> str: has_env_key = bool(env_overrides.get("HL_PRIVATE_KEY") or os.environ.get("HL_PRIVATE_KEY")) has_web_auth = bool(env_overrides.get("NUNCHI_WEB_AUTH_PAIR_TOKEN")) and bool(env_overrides.get("NUNCHI_WEB_AUTH_ADDRESS")) + has_view_only = bool(env_overrides.get("HL_VIEW_AS_USER")) + permission_tier = str(env_overrides.get("NUNCHI_TRADING_PERMISSION_TIER") or "").strip().lower() keystores = list_keystores() if has_env_key: ok_items.append("HL_PRIVATE_KEY set") elif has_web_auth: ok_items.append("web-auth pairing context provided") + elif has_view_only and permission_tier == "read_only": + ok_items.append(f"view-only context provided ({env_overrides.get('HL_VIEW_AS_USER')})") elif keystores: ok_items.append(f"Keystore found ({len(keystores)} keys)") else: diff --git a/scripts/mcp_workload_experiment.py b/scripts/mcp_workload_experiment.py new file mode 100644 index 0000000..4af6758 --- /dev/null +++ b/scripts/mcp_workload_experiment.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Controlled live MCP workload experiment. + +Seeds internal experiment subscriptions in web-auth, mints real hosted MCP +gateway tokens, runs hedge/maker/taker tool mixes, and fetches the internal +margin dashboard for evidence-backed tier analysis. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +WEB_AUTH_URL = "https://web-auth-production-4d1b.up.railway.app" +GATEWAY_URL = "https://agent.nunchi.trade" +SERVER_ID = "nunchi_trading" +ADDRESSES = [ + "0xEb1Ba7Fc58b3416361a0EE07d140c91410c0AA8c", + "0x2f0ba6686208Cb31a319F3F54587b1eF1EF0F87e", +] + +PROFILES: dict[str, dict[str, Any]] = { + "hedge": { + "planId": "hosted-mcp-inference-growth", + "tier": "Growth inference tier", + "monthly": {"seats": 2, "mcpCalls": 8700, "paidComputeCalls": 30, "safetyGatedCalls": 30, "inferenceUsd": 5.0}, + "calls": [ + ("setup_check", {}), + ("account", {"mainnet": False}), + ("status", {}), + ("funding_hedge_propose", {"coin": "BTC", "mainnet": False}), + ("funding_hedge_backtest", {"coin": "BTC", "days": 7, "notional": 100000}), + ("funding_hedge_execute", {"coin": "BTC", "dry_run": True, "mainnet": False, "confirmed": True}), + ("openrouter_chat", {"prompt": "Summarize the risk posture for a small funding hedge experiment in one sentence.", "max_tokens": 24, "temperature": 0.1}), + ], + }, + "maker": { + "planId": "hosted-mcp-tools-team", + "tier": "Team BYO-inference tier", + "monthly": {"seats": 5, "mcpCalls": 45150, "paidComputeCalls": 150, "safetyGatedCalls": 15000, "inferenceUsd": 0.0}, + "calls": [ + ("setup_check", {}), + ("strategies", {}), + ("account", {"mainnet": False}), + ("status", {}), + ("radar_run", {"mock": True}), + ("reflect_run", {"since": "2026-07-01"}), + ("run_strategy", {"strategy": "maker", "instrument": "ETH-PERP", "max_ticks": 1, "mock": True, "dry_run": True}), + ("trade", {"instrument": "ETH-PERP", "side": "buy", "size": 0.001, "mainnet": False, "confirmed": True}), + ("trade", {"instrument": "ETH-PERP", "side": "sell", "size": 0.001, "mainnet": False, "confirmed": True}), + ], + }, + "taker": { + "planId": "hosted-mcp-inference-growth", + "tier": "Growth inference tier", + "monthly": {"seats": 2, "mcpCalls": 5500, "paidComputeCalls": 500, "safetyGatedCalls": 1000, "inferenceUsd": 15.0}, + "calls": [ + ("setup_check", {}), + ("account", {"mainnet": False}), + ("status", {}), + ("radar_run", {"mock": True}), + ("openrouter_chat", {"prompt": "Choose one cautious taker setup from mocked risk context and explain why in one sentence.", "max_tokens": 24, "temperature": 0.1}), + ("trade", {"instrument": "BTC-PERP", "side": "buy", "size": 0.001, "mainnet": False, "confirmed": True}), + ], + }, +} + + +class HttpError(RuntimeError): + def __init__(self, status: int, body: Any) -> None: + super().__init__(str(body)) + self.status = status + self.body = body + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--web-auth-url", default=os.environ.get("WEB_AUTH_PAIR_API_URL") or WEB_AUTH_URL) + parser.add_argument("--gateway-url", default=os.environ.get("NUNCHI_MCP_GATEWAY_URL") or GATEWAY_URL) + parser.add_argument("--connect-token", default=os.environ.get("NUNCHI_CONNECT_API_TOKEN", "")) + parser.add_argument("--metering-token", default=os.environ.get("INTERNAL_MCP_EXPERIMENT_TOKEN") or os.environ.get("INTERNAL_COSTING_DASHBOARD_TOKEN", "")) + parser.add_argument("--costing-token", default=os.environ.get("INTERNAL_COSTING_DASHBOARD_TOKEN", "")) + parser.add_argument("--addresses", nargs="+", default=ADDRESSES) + parser.add_argument("--profiles", nargs="+", default=list(PROFILES)) + parser.add_argument("--cycles", type=int, default=1) + parser.add_argument("--sleep-after", type=float, default=10.0) + parser.add_argument("--run-id", default=datetime.now(timezone.utc).strftime("mcp-workload-%Y%m%dT%H%M%SZ")) + parser.add_argument("--output", default="") + parser.add_argument("--strict", action="store_true") + parser.add_argument("--cleanup", action="store_true", help="Delete internal experiment subscriptions after collecting results.") + args = parser.parse_args() + + for name, value in { + "NUNCHI_CONNECT_API_TOKEN": args.connect_token, + "INTERNAL_MCP_EXPERIMENT_TOKEN": args.metering_token, + "INTERNAL_COSTING_DASHBOARD_TOKEN": args.costing_token, + }.items(): + if not value: + raise SystemExit(f"missing {name}") + + results: dict[str, Any] = { + "runId": args.run_id, + "generatedAt": datetime.now(timezone.utc).isoformat(), + "addresses": args.addresses, + "inputs": {"cycles": args.cycles, "webAuthUrl": args.web_auth_url, "gatewayUrl": args.gateway_url}, + "profiles": [], + } + for profile_name in args.profiles: + profile = PROFILES[profile_name] + for index, address in enumerate(args.addresses): + results["profiles"].append(run_profile(args, profile_name, profile, address, index)) + + if args.sleep_after: + time.sleep(args.sleep_after) + results["marginDashboard"] = http_json("GET", f"{args.web_auth_url.rstrip('/')}/api/billing/subscription/margin-dashboard", token=args.costing_token) + results["tierExpectations"] = derive_expectations(results) + if args.cleanup: + results["cleanup"] = cleanup_experiment(args) + + out = Path(args.output or f"tmp/{args.run_id}.json") + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(results, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps({"ok": True, "output": str(out), "profileRuns": len(results["profiles"])}, indent=2)) + failures = [call for profile in results["profiles"] for call in profile["calls"] if not call["ok"]] + if failures and args.strict: + print(json.dumps({"failedCalls": failures[:20]}, indent=2), file=sys.stderr) + return 1 + return 0 + + +def run_profile(args: argparse.Namespace, profile_name: str, profile: dict[str, Any], address: str, index: int) -> dict[str, Any]: + user_id = f"mcp-exp-{profile_name}-{address[-8:].lower()}" + subscription_id = f"{args.run_id}-{profile_name}-{index}" + seed_subscription(args, profile, user_id, address, subscription_id) + minted = connect_gateway(args, profile_name, profile, user_id, address, subscription_id) + calls = [] + for cycle in range(args.cycles): + for tool, body in profile["calls"]: + started = time.perf_counter() + try: + response = http_json("POST", f"{args.gateway_url.rstrip('/')}/v1/servers/{SERVER_ID}/tools/{tool}/call", token=minted["token"], body=body, timeout=60) + content_error = tool_content_error(response) + calls.append({ + "cycle": cycle, + "tool": tool, + "ok": content_error is None, + "status": 200, + "elapsedMs": round((time.perf_counter() - started) * 1000, 3), + "error": content_error, + "preview": preview(response), + }) + except HttpError as exc: + calls.append({"cycle": cycle, "tool": tool, "ok": False, "status": exc.status, "elapsedMs": round((time.perf_counter() - started) * 1000, 3), "error": preview(exc.body)}) + return { + "profile": profile_name, + "address": address, + "userId": user_id, + "accountId": address, + "subscriptionId": subscription_id, + "planId": profile["planId"], + "expectedTier": profile["tier"], + "monthlyExpectation": profile["monthly"], + "gatewayTokenId": minted.get("token_id"), + "calls": calls, + } + + +def seed_subscription(args: argparse.Namespace, profile: dict[str, Any], user_id: str, account_id: str, subscription_id: str) -> None: + http_json( + "POST", + f"{args.web_auth_url.rstrip('/')}/api/internal/costing/experiment-subscription", + token=args.costing_token, + body={ + "userId": user_id, + "accountId": account_id, + "subscriptionId": subscription_id, + "planId": profile["planId"], + "status": "active", + "experimentName": args.run_id, + }, + ) + + +def connect_gateway(args: argparse.Namespace, profile_name: str, profile: dict[str, Any], user_id: str, account_id: str, subscription_id: str) -> dict[str, Any]: + web = args.web_auth_url.rstrip("/") + return http_json( + "POST", + f"{args.gateway_url.rstrip('/')}/v1/connect/hosted-trading", + token=args.connect_token, + headers={"x-nunchi-user-id": user_id}, + body={ + "workspace_id": args.run_id, + "agent_id": f"experiment-{profile_name}", + "permission_tier": "testnet_trading", + "network": "testnet", + "ttl_seconds": 3600, + "max_order_size": 0.01, + "max_strategy_ticks": 5, + "require_confirmation": False, + "account_id": account_id, + "subscription_id": subscription_id, + "plan_id": profile["planId"], + "metering_status_url": f"{web}/api/metering/status", + "metering_usage_url": f"{web}/api/metering/usage", + "metering_seats_register_url": f"{web}/api/metering/seats/register", + "metering_seats_release_url": f"{web}/api/metering/seats/release", + "metering_token": args.metering_token, + }, + ) + + +def derive_expectations(results: dict[str, Any]) -> dict[str, Any]: + account_rows = results.get("marginDashboard", {}).get("rows", []) + wanted = {run["subscriptionId"] for run in results["profiles"]} + experiment_rows = [row for row in account_rows if row.get("subscriptionId") in wanted] + return { + "source": "Real gateway tool calls plus web-auth internal margin dashboard", + "profileGuidance": [ + { + "profile": run["profile"], + "address": run["address"], + "subscriptionId": run["subscriptionId"], + "planId": run["planId"], + "expectedTier": run["expectedTier"], + "successfulCalls": sum(1 for call in run["calls"] if call["ok"]), + "failedCalls": sum(1 for call in run["calls"] if not call["ok"]), + "monthlyExpectation": run["monthlyExpectation"], + } + for run in results["profiles"] + ], + "experimentAccounts": experiment_rows, + "byTier": results.get("marginDashboard", {}).get("byTier", []), + } + + +def cleanup_experiment(args: argparse.Namespace) -> dict[str, Any]: + return http_json( + "POST", + f"{args.web_auth_url.rstrip('/')}/api/internal/costing/experiment-subscriptions/cleanup", + token=args.costing_token, + body={"experimentName": args.run_id}, + ) + + +def http_json(method: str, url: str, *, token: str = "", headers: dict[str, str] | None = None, body: Any = None, timeout: int = 30) -> dict[str, Any]: + data = json.dumps(body).encode("utf-8") if body is not None else None + request = urllib.request.Request( + url, + data=data, + method=method, + headers={ + "accept": "application/json", + **({"content-type": "application/json"} if body is not None else {}), + **({"authorization": f"Bearer {token}"} if token else {}), + **(headers or {}), + }, + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + raw = response.read().decode("utf-8") + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as exc: + raw = exc.read().decode("utf-8", errors="replace") + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + parsed = raw + raise HttpError(exc.code, parsed) from exc + + +def preview(value: Any, limit: int = 500) -> str: + text = value if isinstance(value, str) else json.dumps(value, sort_keys=True) + return text[:limit] + + +def tool_content_error(response: dict[str, Any]) -> str | None: + result = response.get("result") + if not isinstance(result, dict): + return None + structured = result.get("structuredContent") + if result.get("isError") is True and isinstance(structured, dict) and structured.get("error"): + return preview(str(structured.get("error")), 300) + contents = result.get("content") + if not isinstance(contents, list): + return None + text = "\n".join( + str(item.get("text", "")) + for item in contents + if isinstance(item, dict) + ) + lowered = text.lower() + markers = [ + '"error"', + "traceback", + "requires a signing context", + "no signing context", + "unknown tool", + ] + if any(marker in lowered for marker in markers): + return preview(text, 300) + return None + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_mcp_gateway_context.py b/tests/test_mcp_gateway_context.py index 4281594..102962f 100644 --- a/tests/test_mcp_gateway_context.py +++ b/tests/test_mcp_gateway_context.py @@ -39,6 +39,7 @@ def test_trusted_gateway_headers_become_scoped_env(monkeypatch): assert env["NUNCHI_WEB_AUTH_PAIR_TOKEN"] == "pair-token" assert env["NUNCHI_WEB_AUTH_ADDRESS"] == "0x" + "2" * 40 + assert env["HL_VIEW_AS_USER"] == "0x" + "2" * 40 assert env["NUNCHI_MAX_ORDER_SIZE"] == "0.5" assert env["NUNCHI_MAX_STRATEGY_TICKS"] == "12" policy = json.loads(env["NUNCHI_SESSION_POLICY"]) @@ -47,6 +48,24 @@ def test_trusted_gateway_headers_become_scoped_env(monkeypatch): assert "trade" in policy["allowed_actions"] +def test_trusted_gateway_account_id_becomes_view_only_address(monkeypatch): + from cli.mcp_server import _trusted_context_env_overrides + + account_id = "0x" + "6" * 40 + monkeypatch.setenv("NUNCHI_RUNNER_CONTEXT_SECRET", "shared-secret") + ctx = _ctx({ + "x-nunchi-runner-context-secret": "shared-secret", + "x-nunchi-account-id": account_id, + "x-nunchi-trading-permission-tier": "read_only", + "x-nunchi-trading-network": "testnet", + }) + + env = _trusted_context_env_overrides(ctx) + + assert env["NUNCHI_ACCOUNT_ID"] == account_id + assert env["HL_VIEW_AS_USER"] == account_id + + def test_context_limits_fail_closed_without_signing_context(monkeypatch, tmp_path): from cli.mcp_server import _context_limit_error @@ -145,6 +164,22 @@ def test_entrypoint_trade_fails_closed_without_signing_context(monkeypatch, tmp_ assert status == 200 assert "requires a signing context" in response["result"]["content"][0]["text"] + assert response["result"]["isError"] is True + assert "requires a signing context" in response["result"]["structuredContent"]["error"] + + +def test_entrypoint_structures_json_tool_errors(): + from scripts.entrypoint import _mcp_tool_result + + result = _mcp_tool_result(json.dumps({"error": "boom", "code": "example_error"})) + + assert result["isError"] is True + assert result["structuredContent"] == { + "ok": False, + "error": "boom", + "code": "example_error", + } + assert result["content"][0]["text"] == '{"error": "boom", "code": "example_error"}' def test_entrypoint_trade_forwards_trusted_context_to_subprocess(monkeypatch, tmp_path): @@ -206,6 +241,8 @@ def test_entrypoint_funding_hedge_execute_refuses_without_confirm(): assert status == 200 assert "confirmed=true" in response["result"]["content"][0]["text"] + assert response["result"]["isError"] is True + assert "confirmed=true" in response["result"]["structuredContent"]["error"] def test_entrypoint_funding_hedge_execute_confirmed_dry_run_forwards_to_cli(monkeypatch, tmp_path): @@ -249,6 +286,7 @@ def fake_run_hl(*args, timeout=30, env_overrides=None): assert captured["timeout"] == 120 assert captured["env_overrides"]["NUNCHI_WEB_AUTH_PAIR_TOKEN"] == "pair-token" assert captured["env_overrides"]["NUNCHI_WEB_AUTH_ADDRESS"] == "0x" + "5" * 40 + assert captured["env_overrides"]["HL_VIEW_AS_USER"] == "0x" + "5" * 40 def test_entrypoint_funding_hedge_execute_confirmed_dry_run_allows_keyless_preview(monkeypatch, tmp_path): From 328a54a19927fe0e085d288d98a09cf8d999af66 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 8 Jul 2026 08:00:11 -0400 Subject: [PATCH 5/5] chore: remove all Hermes/OpenClaw deploy references Delete archived hermes-railway and openclaw-railway deploy templates entirely and strip remaining references from docs, telemetry, skills, and README. Product surface is CLI + MCP only. Co-authored-by: Cursor --- README.md | 31 +- archive/README.md | 4 +- archive/deploy/hermes-railway/Dockerfile | 92 ------ archive/deploy/hermes-railway/package.json | 14 - archive/deploy/hermes-railway/railway.toml | 23 -- .../deploy/hermes-railway/src/bootstrap.mjs | 139 -------- archive/deploy/hermes-railway/src/gateway.js | 114 ------- archive/deploy/hermes-railway/src/onboard.js | 133 -------- archive/deploy/hermes-railway/src/server.js | 273 ---------------- archive/deploy/hermes-railway/src/status.js | 63 ---- .../deploy/hermes-railway/workspace/AGENTS.md | 47 --- .../hermes-railway/workspace/BOOTSTRAP.md | 39 --- .../deploy/hermes-railway/workspace/SOUL.md | 17 - .../deploy/hermes-railway/workspace/TOOLS.md | 61 ---- archive/deploy/openclaw-railway/.env.example | 18 -- archive/deploy/openclaw-railway/Dockerfile | 87 ----- archive/deploy/openclaw-railway/package.json | 13 - archive/deploy/openclaw-railway/railway.toml | 23 -- .../deploy/openclaw-railway/src/bootstrap.mjs | 286 ----------------- .../deploy/openclaw-railway/src/gateway.js | 100 ------ .../deploy/openclaw-railway/src/onboard.js | 154 --------- archive/deploy/openclaw-railway/src/server.js | 297 ------------------ archive/deploy/openclaw-railway/src/status.js | 62 ---- .../openclaw-railway/workspace/AGENTS.md | 47 --- .../openclaw-railway/workspace/BOOTSTRAP.md | 39 --- .../deploy/openclaw-railway/workspace/SOUL.md | 17 - .../openclaw-railway/workspace/TOOLS.md | 59 ---- cli/api/status_reader.py | 1 - cli/hedge_display.py | 2 +- cli/skill.md | 14 +- cli/telemetry.py | 4 - docs/RUNBOOK.md | 3 +- docs/api-reference.md | 12 +- tests/test_telemetry.py | 4 - 34 files changed, 22 insertions(+), 2270 deletions(-) delete mode 100644 archive/deploy/hermes-railway/Dockerfile delete mode 100644 archive/deploy/hermes-railway/package.json delete mode 100644 archive/deploy/hermes-railway/railway.toml delete mode 100644 archive/deploy/hermes-railway/src/bootstrap.mjs delete mode 100644 archive/deploy/hermes-railway/src/gateway.js delete mode 100644 archive/deploy/hermes-railway/src/onboard.js delete mode 100644 archive/deploy/hermes-railway/src/server.js delete mode 100644 archive/deploy/hermes-railway/src/status.js delete mode 100644 archive/deploy/hermes-railway/workspace/AGENTS.md delete mode 100644 archive/deploy/hermes-railway/workspace/BOOTSTRAP.md delete mode 100644 archive/deploy/hermes-railway/workspace/SOUL.md delete mode 100644 archive/deploy/hermes-railway/workspace/TOOLS.md delete mode 100644 archive/deploy/openclaw-railway/.env.example delete mode 100644 archive/deploy/openclaw-railway/Dockerfile delete mode 100644 archive/deploy/openclaw-railway/package.json delete mode 100644 archive/deploy/openclaw-railway/railway.toml delete mode 100644 archive/deploy/openclaw-railway/src/bootstrap.mjs delete mode 100644 archive/deploy/openclaw-railway/src/gateway.js delete mode 100644 archive/deploy/openclaw-railway/src/onboard.js delete mode 100644 archive/deploy/openclaw-railway/src/server.js delete mode 100644 archive/deploy/openclaw-railway/src/status.js delete mode 100644 archive/deploy/openclaw-railway/workspace/AGENTS.md delete mode 100644 archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md delete mode 100644 archive/deploy/openclaw-railway/workspace/SOUL.md delete mode 100644 archive/deploy/openclaw-railway/workspace/TOOLS.md diff --git a/README.md b/README.md index 4cb91f9..2252cbb 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ --- -Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. K2 inputs come from HL funding history only (no SEDA). Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, an [OpenClaw](https://agentskills.io) or [Hermes](https://github.com/NousResearch/hermes-agent) agent toolset, or a standalone MCP server. +Ship CFI v2 funding-rate hedging on [Hyperliquid](https://hyperliquid.xyz) perps and Paragon HIP-3 BTCSWP swap markets (`para:BTCSWP` mainnet, `osrs:BTCSWP` / `yex:BTCSWP` testnet). Primary surface: `hl hedge propose|execute|status|auto|backtest`. K2 inputs come from HL funding history only (no SEDA). Also exposes Guard, Radar, Pulse, APEX, and REFLECT as optional operator tools. Works as a standalone CLI, a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill, or a standalone MCP server. -Legacy market-making (`quoting_engine`), SEDA oracle, and Hermes/OpenClaw Railway deploy templates were archived 2026-07-02 under `archive/` — see `archive/README.md`. +Legacy market-making (`quoting_engine`) and SEDA oracle were archived 2026-07-02 under `archive/` — see `archive/README.md`. --- @@ -195,12 +195,6 @@ https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/guard/SKILL https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/reflect/SKILL.md ``` -### Install a skill (OpenClaw / ClawHub) - -```bash -clawhub install nunchi-trade/yex-trader -``` - ### Install a skill (Claude Code) ```bash @@ -515,10 +509,10 @@ Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Pyt ### Use from any agent harness -`hl mcp serve` is **harness-neutral** — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [OpenClaw](https://agentskills.io), and [Hermes](https://github.com/NousResearch/hermes-agent) consume the exact same tools. To point a **Hermes** agent (or any MCP host) at this repo, register the server in its config: +`hl mcp serve` is **harness-neutral** — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Cursor, Codex, and any MCP-compatible client consume the exact same tools. Register the server in your MCP host config: ```yaml -# Hermes ~/.hermes/config.yaml (or any MCP harness) +# Any MCP harness (e.g. ~/.cursor/mcp.json, Claude Code settings) mcp_servers: nunchi_trading: command: python3 @@ -526,8 +520,6 @@ mcp_servers: # cwd: /path/to/agent-cli # if not launched from the repo root ``` -This is exactly what the legacy `archive/deploy/hermes-railway` template auto-generates on boot (alongside `platform_toolsets`), so a deployed Hermes agent is wired out of the box — no OpenClaw-specific assumptions anywhere in the MCP server. - ### HTTP API & SSE Every deployed agent also exposes an HTTP REST API and SSE real-time feed for dashboards, monitoring, and external integrations. A separate leaderboard microservice tracks agent PnL rankings. @@ -539,7 +531,7 @@ Every deployed agent also exposes an HTTP REST API and SSE real-time feed for da ## Deploy on Railway The subscription product uses Railway as a **shared MCP tools runtime**, not as -a per-user Hermes/OpenClaw/autonomous-agent host. The top-level Railway template +a per-user autonomous-agent host. The top-level Railway template defaults to `RUN_MODE=mcp`; Nunchi operates this runner pool behind `mcp-gateway`, while users run their own Cursor, Claude, Codex, or local `agent-cli` clients. @@ -560,17 +552,6 @@ defaults to `RUN_MODE=mcp`; Nunchi operates this runner pool behind - **mcp** (default) — SSE MCP server for the shared Railway tools runtime. - **apex** / **strategy** — direct autonomous loops for local/self-hosted operators, not the Nunchi subscription product path. -### Legacy Agent Templates - -The OpenClaw and Hermes Railway templates are retained as legacy/reference -self-host templates. They are not part of the new hosted MCP subscription -architecture because they provision user-facing conversational/autonomous -agents. Do not expose them as the paid Nunchi product path. - -For legacy experiments, use `archive/deploy/openclaw-railway/railway.toml` or -`archive/deploy/hermes-railway/railway.toml` as the config-as-code path and keep the -Railway build root at the repo root. - --- ## Architecture @@ -671,7 +652,7 @@ pytest tests/ -v # 1300+ tests ## Attribution -Inspired by openclaw, senpi, and claude code. +Inspired by senpi and claude code. --- ## Links diff --git a/archive/README.md b/archive/README.md index 7cd75ff..6cfada1 100644 --- a/archive/README.md +++ b/archive/README.md @@ -1,15 +1,13 @@ # Archived legacy components **Date:** 2026-07-02 -**Reason:** Product focus narrowed to CFI v2 funding-rate hedge only (`hl hedge`, `cfi_hedge` strategy). Market-making stack, SEDA oracle, and legacy agent deploy templates are no longer maintained in the active tree. +**Reason:** Product focus narrowed to CFI v2 funding-rate hedge only (`hl hedge`, `cfi_hedge` strategy). Market-making stack and SEDA oracle are no longer maintained in the active tree. ## What moved here | Path | Description | |------|-------------| | `archive/quoting_engine/` | Full market-making stack (fair value, spreads, ladders, toxicity, funding feeds). Includes `feeds/seda_oracle.py` (deprecated). | -| `archive/deploy/hermes-railway/` | Hermes agent gateway Railway template | -| `archive/deploy/openclaw-railway/` | OpenClaw agent gateway Railway template | ## Active replacement diff --git a/archive/deploy/hermes-railway/Dockerfile b/archive/deploy/hermes-railway/Dockerfile deleted file mode 100644 index c00f246..0000000 --- a/archive/deploy/hermes-railway/Dockerfile +++ /dev/null @@ -1,92 +0,0 @@ -# Hermes Agent + agent-cli — Railway one-click image -# -# Pinned upstream: github.com/NousResearch/hermes-agent @ ${HERMES_GIT_REF} -# Pinned interface: hermes dashboard --host 0.0.0.0 --port --insecure --no-open -# -# Layout mirrors deploy/openclaw-railway: an Express front door at $PORT proxies -# to the internal Hermes dashboard, while preserving Nunchi-specific /api/* -# endpoints driven by agent-cli's status_reader. - -FROM node:22-bookworm AS hermes-build - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - git ca-certificates curl python3 python3-venv python3-pip make g++ \ - && rm -rf /var/lib/apt/lists/* - -# uv (matches upstream Dockerfile) for fast, reproducible Python installs -ADD https://astral.sh/uv/install.sh /tmp/uv-install.sh -RUN sh /tmp/uv-install.sh && mv /root/.local/bin/uv /root/.local/bin/uvx /usr/local/bin/ - -WORKDIR /hermes -ARG HERMES_GIT_REF=v2026.4.30 -RUN git clone --depth 1 --branch "${HERMES_GIT_REF}" https://github.com/NousResearch/hermes-agent.git . - -# Build the web dashboard SPA + TUI assets. -ENV npm_config_install_links=false -RUN npm install --prefer-offline --no-audit \ - && (cd web && npm install --prefer-offline --no-audit && npm run build) \ - && (cd ui-tui && npm install --prefer-offline --no-audit && npm run build) \ - && npm cache clean --force - - -# Runtime image -FROM node:22-bookworm -ENV NODE_ENV=production -ENV PYTHONUNBUFFERED=1 - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates curl build-essential gcc g++ make procps file git \ - python3 python3-pip python3-venv pkg-config sudo ripgrep tini \ - && rm -rf /var/lib/apt/lists/* \ - && ln -sf /usr/bin/python3 /usr/local/bin/python - -# uv (used for both Hermes and agent-cli installs) -ADD https://astral.sh/uv/install.sh /tmp/uv-install.sh -RUN sh /tmp/uv-install.sh && mv /root/.local/bin/uv /root/.local/bin/uvx /usr/local/bin/ - -# Copy Hermes source + pre-built UI assets -COPY --from=hermes-build /hermes /hermes - -# Install Hermes into a venv, including MCP + messaging extras (Telegram/Discord/Slack/etc). -WORKDIR /hermes -RUN uv venv \ - && uv pip install --no-cache-dir -e ".[mcp,messaging,web,cli]" - -# Make hermes CLI globally available. -RUN printf '%s\n' '#!/usr/bin/env bash' \ - 'source /hermes/.venv/bin/activate' \ - 'exec hermes "$@"' \ - > /usr/local/bin/hermes && chmod +x /usr/local/bin/hermes - -# Install agent-cli (the Nunchi trading CLI) into the same venv so the -# `nunchi_trading` MCP server (`python3 -m cli.main mcp serve`) and the -# /api/* status_reader endpoints can both find it. -# NOTE: build context MUST be the repo root (see deploy/hermes-railway/railway.toml -# dockerfilePath + Railway service Root Directory = repo root). All COPY paths below -# are therefore relative to the repo root, not to deploy/hermes-railway/. -WORKDIR /agent-cli -COPY . . -# `uv venv` (line ~54) creates the venv WITHOUT pip, so /hermes/.venv/bin/pip -# doesn't exist. Install into that same venv with `uv pip` (same mechanism used -# to install Hermes above), targeting it via VIRTUAL_ENV. -RUN VIRTUAL_ENV=/hermes/.venv uv pip install --no-cache-dir -e ".[mcp]" - -# Express wrapper -WORKDIR /app -COPY deploy/hermes-railway/package.json ./ -RUN npm install --production - -COPY deploy/hermes-railway/src ./src - -# Workspace defaults — copied to volume on first boot -COPY deploy/hermes-railway/workspace /opt/workspace-defaults - -RUN mkdir -p /data -ENV PORT=8080 -EXPOSE 8080 - -# tini reaps MCP stdio subprocesses (matches upstream Hermes Dockerfile) -ENTRYPOINT ["/usr/bin/tini", "-g", "--"] -CMD ["node", "src/server.js"] diff --git a/archive/deploy/hermes-railway/package.json b/archive/deploy/hermes-railway/package.json deleted file mode 100644 index 4bdf4e6..0000000 --- a/archive/deploy/hermes-railway/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "nunchi-hermes-railway", - "version": "0.1.0", - "description": "One-click Hermes Agent (Nous Research) trading deployment for Hyperliquid — powered by Nunchi agent-cli", - "private": true, - "scripts": { - "start": "node src/server.js" - }, - "dependencies": { - "express": "^4.21.0", - "http-proxy": "^1.18.1", - "js-yaml": "^4.1.0" - } -} diff --git a/archive/deploy/hermes-railway/railway.toml b/archive/deploy/hermes-railway/railway.toml deleted file mode 100644 index 16afa37..0000000 --- a/archive/deploy/hermes-railway/railway.toml +++ /dev/null @@ -1,23 +0,0 @@ -[build] -builder = "dockerfile" -# Build context is the repo root so the Dockerfile can COPY the agent-cli source. -# Set this service's Root Directory to the repo root (/) in Railway, not -# archive/deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "archive/deploy/hermes-railway/Dockerfile" - -[deploy] -healthcheckPath = "/health" -healthcheckTimeout = 300 -restartPolicyType = "on_failure" - -[[deploy.volumes]] -mountPath = "/data" - -[variables] -PORT = "8080" -HL_TESTNET = "true" -INTERNAL_GATEWAY_HOST = "127.0.0.1" -INTERNAL_GATEWAY_PORT = "9119" -HERMES_HOME = "/data/.hermes" -HERMES_DASHBOARD_HOST = "127.0.0.1" -HERMES_DASHBOARD_PORT = "9119" diff --git a/archive/deploy/hermes-railway/src/bootstrap.mjs b/archive/deploy/hermes-railway/src/bootstrap.mjs deleted file mode 100644 index 7c60579..0000000 --- a/archive/deploy/hermes-railway/src/bootstrap.mjs +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Bootstrap — auto-configure Hermes Agent with the Nunchi trading MCP server. - * - * Hermes splits config across two files in HERMES_HOME: - * - .env — provider API keys, TELEGRAM_BOT_TOKEN, etc. - * - config.yaml — model.provider, mcp_servers, platform_toolsets - * - * We seed both from Railway env vars on startup. User edits to either file - * are preserved across restarts (we only write keys we own). - */ -import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, readdirSync } from "fs"; -import { join } from "path"; -import { execSync } from "child_process"; -import yaml from "js-yaml"; - -const HERMES_HOME = process.env.HERMES_HOME || "/data/.hermes"; -const WORKSPACE_DIR = join(HERMES_HOME, "workspace"); -const WORKSPACE_DEFAULTS = "/opt/workspace-defaults"; -const CONFIG_PATH = join(HERMES_HOME, "config.yaml"); -const ENV_PATH = join(HERMES_HOME, ".env"); - -// AI_PROVIDER → (Hermes provider name, env var Hermes reads the key from) -const PROVIDER_MAP = { - anthropic: { provider: "anthropic", envKey: "ANTHROPIC_API_KEY" }, - openai: { provider: "openrouter", envKey: "OPENAI_API_KEY" }, - openrouter: { provider: "openrouter", envKey: "OPENROUTER_API_KEY" }, - gemini: { provider: "gemini", envKey: "GEMINI_API_KEY" }, - google: { provider: "gemini", envKey: "GOOGLE_API_KEY" }, - nous: { provider: "nous-api", envKey: "NOUS_API_KEY" }, - zai: { provider: "zai", envKey: "GLM_API_KEY" }, - kimi: { provider: "kimi-coding", envKey: "KIMI_API_KEY" }, - huggingface: { provider: "huggingface", envKey: "HF_TOKEN" }, -}; - -export async function bootstrap() { - console.log("[bootstrap] Starting auto-configuration..."); - - for (const dir of [ - HERMES_HOME, - WORKSPACE_DIR, - join(HERMES_HOME, "skills"), - join(HERMES_HOME, "memories"), - join(HERMES_HOME, "logs"), - join(HERMES_HOME, "sessions"), - ]) { - mkdirSync(dir, { recursive: true }); - } - - if (existsSync(WORKSPACE_DEFAULTS)) { - for (const file of readdirSync(WORKSPACE_DEFAULTS)) { - const dest = join(WORKSPACE_DIR, file); - if (!existsSync(dest)) { - copyFileSync(join(WORKSPACE_DEFAULTS, file), dest); - console.log(`[bootstrap] Synced ${file} to workspace`); - } - } - } - - writeEnvFile(); - writeConfigYaml(); - - // Best-effort Hyperliquid builder fee approval (idempotent) - if (process.env.HL_PRIVATE_KEY) { - try { - const mainnet = (process.env.HL_TESTNET || "true").toLowerCase() === "false"; - const args = mainnet ? ["builder", "approve", "--mainnet"] : ["builder", "approve"]; - execSync(`python3 -m cli.main ${args.join(" ")}`, { - timeout: 30000, - cwd: "/agent-cli", - stdio: "pipe", - }); - console.log("[bootstrap] Builder fee approval sent"); - } catch { - // best-effort - } - } - - console.log("[bootstrap] Configuration complete"); -} - -function writeEnvFile() { - const aiProvider = (process.env.AI_PROVIDER || "anthropic").toLowerCase(); - const aiKey = process.env.AI_API_KEY || ""; - const providerInfo = PROVIDER_MAP[aiProvider] || PROVIDER_MAP.anthropic; - - const lines = []; - if (aiKey) lines.push(`${providerInfo.envKey}=${aiKey}`); - if (process.env.TELEGRAM_BOT_TOKEN) lines.push(`TELEGRAM_BOT_TOKEN=${process.env.TELEGRAM_BOT_TOKEN}`); - if (process.env.DISCORD_BOT_TOKEN) lines.push(`DISCORD_BOT_TOKEN=${process.env.DISCORD_BOT_TOKEN}`); - if (process.env.SLACK_BOT_TOKEN) lines.push(`SLACK_BOT_TOKEN=${process.env.SLACK_BOT_TOKEN}`); - - if (lines.length === 0) { - console.log("[bootstrap] No credentials to write to .env"); - return; - } - - writeFileSync(ENV_PATH, lines.join("\n") + "\n", { mode: 0o600 }); - console.log(`[bootstrap] Wrote ${lines.length} entries to .env`); -} - -function writeConfigYaml() { - const aiProvider = (process.env.AI_PROVIDER || "anthropic").toLowerCase(); - const providerInfo = PROVIDER_MAP[aiProvider] || PROVIDER_MAP.anthropic; - - const existing = existsSync(CONFIG_PATH) - ? (yaml.load(readFileSync(CONFIG_PATH, "utf-8")) || {}) - : {}; - - const config = { - ...existing, - model: { - ...(existing.model || {}), - provider: providerInfo.provider, - ...(process.env.HERMES_MODEL ? { default: process.env.HERMES_MODEL } : {}), - }, - mcp_servers: { - ...(existing.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", - }, - }, - }, - }; - - if (process.env.TELEGRAM_BOT_TOKEN) { - config.platform_toolsets = { - ...(existing.platform_toolsets || {}), - telegram: ["hermes-telegram"], - }; - } - - writeFileSync(CONFIG_PATH, yaml.dump(config, { lineWidth: 120 })); - console.log("[bootstrap] Wrote config.yaml"); -} diff --git a/archive/deploy/hermes-railway/src/gateway.js b/archive/deploy/hermes-railway/src/gateway.js deleted file mode 100644 index 95a467b..0000000 --- a/archive/deploy/hermes-railway/src/gateway.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Gateway lifecycle — spawn, monitor, and restart the Hermes dashboard server. - * - * `hermes dashboard` runs a FastAPI HTTP server (default :9119). With - * --insecure --no-open it's safe to bind to a non-localhost interface inside - * the container; the Express front door at $PORT is what's actually exposed - * to the public network. - */ -const { spawn } = require("child_process"); -const http = require("http"); - -const GATEWAY_HOST = process.env.INTERNAL_GATEWAY_HOST || "127.0.0.1"; -const GATEWAY_PORT = parseInt(process.env.INTERNAL_GATEWAY_PORT || "9119", 10); -const HERMES_BIN = process.env.HERMES_BIN || "/usr/local/bin/hermes"; -const HERMES_HOME = process.env.HERMES_HOME || "/data/.hermes"; - -let gatewayProcess = null; - -function startGateway() { - if (gatewayProcess && !gatewayProcess.killed) { - console.log("[gateway] Already running (pid=%d)", gatewayProcess.pid); - return; - } - - console.log("[gateway] Starting hermes dashboard..."); - - const args = [ - "dashboard", - "--host", GATEWAY_HOST, - "--port", String(GATEWAY_PORT), - "--no-open", - ]; - // Hermes refuses non-localhost binds without --insecure (it exposes API - // keys). Inside a container that's the expected deployment shape — the - // Express wrapper is the public surface, not the dashboard directly. - if (GATEWAY_HOST !== "127.0.0.1" && GATEWAY_HOST !== "localhost") { - args.push("--insecure"); - } - - gatewayProcess = spawn(HERMES_BIN, args, { - env: { - ...process.env, - HERMES_HOME, - NODE_ENV: "production", - }, - stdio: ["ignore", "pipe", "pipe"], - }); - - gatewayProcess.stdout.on("data", (data) => { - const line = data.toString().trim(); - if (line) console.log(`[gateway] ${line}`); - }); - - gatewayProcess.stderr.on("data", (data) => { - const line = data.toString().trim(); - if (line) console.error(`[gateway] ${redactTokens(line)}`); - }); - - gatewayProcess.on("exit", (code, signal) => { - console.log(`[gateway] Exited (code=${code}, signal=${signal})`); - gatewayProcess = null; - }); - - console.log("[gateway] Spawned (pid=%d)", gatewayProcess.pid); -} - -async function waitForGatewayReady(timeoutMs = 60000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - await httpGet(`http://${GATEWAY_HOST}:${GATEWAY_PORT}/`); - return true; - } catch { - await sleep(500); - } - } - throw new Error(`Hermes dashboard did not become ready within ${timeoutMs}ms`); -} - -function getGatewayProcess() { - return gatewayProcess; -} - -function restartGateway() { - if (gatewayProcess && !gatewayProcess.killed) { - gatewayProcess.kill("SIGTERM"); - setTimeout(() => { - if (gatewayProcess && !gatewayProcess.killed) { - gatewayProcess.kill("SIGKILL"); - } - }, 5000); - } - setTimeout(() => startGateway(), 1500); -} - -function httpGet(url) { - return new Promise((resolve, reject) => { - http.get(url, { timeout: 3000 }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => resolve(body)); - }).on("error", reject); - }); -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function redactTokens(str) { - return str.replace(/(?:sk-[a-zA-Z0-9-]{10,}|[a-f0-9]{64})/g, "[REDACTED]"); -} - -module.exports = { startGateway, waitForGatewayReady, getGatewayProcess, restartGateway }; diff --git a/archive/deploy/hermes-railway/src/onboard.js b/archive/deploy/hermes-railway/src/onboard.js deleted file mode 100644 index ef4dd5e..0000000 --- a/archive/deploy/hermes-railway/src/onboard.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Auto-onboard — resolve Telegram chat ID and send the ready message. - * - * Hermes loads config.yaml + .env on dashboard startup, so there's no - * separate onboard CLI step (unlike OpenClaw). This module exists only to - * (a) detect the operator's Telegram chat ID by username and (b) post the - * ready message once per fresh credential set. - */ -const { existsSync, readFileSync, writeFileSync } = require("fs"); -const { join } = require("path"); -const https = require("https"); - -const HERMES_HOME = process.env.HERMES_HOME || "/data/.hermes"; -const FINGERPRINT_PATH = join(HERMES_HOME, ".env-fingerprint"); - -async function autoOnboard() { - const aiProvider = process.env.AI_PROVIDER; - const aiKey = process.env.AI_API_KEY; - - if (!aiProvider || !aiKey) { - console.log("[onboard] No AI_PROVIDER or AI_API_KEY set, skipping auto-onboard"); - return; - } - - const fingerprint = computeFingerprint(); - if (existsSync(FINGERPRINT_PATH)) { - const stored = readFileSync(FINGERPRINT_PATH, "utf-8").trim(); - if (stored === fingerprint) { - console.log("[onboard] Environment unchanged, skipping onboard"); - return; - } - } - - console.log("[onboard] Running auto-onboard..."); - - let telegramChatId = null; - if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_USERNAME) { - try { - telegramChatId = await resolveTelegramChatId( - process.env.TELEGRAM_BOT_TOKEN, - process.env.TELEGRAM_USERNAME, - ); - if (telegramChatId) { - console.log(`[onboard] Resolved Telegram chat ID: ${telegramChatId}`); - const userMd = `# User\n\nTelegram chat ID: ${telegramChatId}\nUsername: ${process.env.TELEGRAM_USERNAME}\n`; - writeFileSync(join(HERMES_HOME, "workspace", "USER.md"), userMd); - } - } catch (err) { - console.warn("[onboard] Could not resolve Telegram chat ID:", err.message); - } - } - - if (process.env.TELEGRAM_BOT_TOKEN) { - try { - await sendTelegramMessage( - process.env.TELEGRAM_BOT_TOKEN, - "Nunchi Hermes agent is ready. Say 'hl apex run' to start autonomous trading, or 'hl radar once' to scan for opportunities.", - telegramChatId, - ); - console.log("[onboard] Sent ready message to Telegram"); - } catch (err) { - console.warn("[onboard] Could not send Telegram message:", err.message); - } - } - - writeFileSync(FINGERPRINT_PATH, fingerprint); - console.log("[onboard] Onboarding complete"); -} - -function computeFingerprint() { - const crypto = require("crypto"); - const parts = [ - process.env.AI_PROVIDER || "", - (process.env.AI_API_KEY || "").slice(-8), - process.env.TELEGRAM_BOT_TOKEN ? "tg" : "", - process.env.HL_TESTNET || "true", - ]; - return crypto.createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 16); -} - -async function resolveTelegramChatId(botToken, username) { - const cleanUsername = username.replace("@", "").toLowerCase(); - const data = await fetchJson(`https://api.telegram.org/bot${botToken}/getUpdates?limit=50`); - if (!data.ok || !data.result) return null; - - for (const update of data.result) { - const msg = update.message || update.my_chat_member; - if (!msg || !msg.from) continue; - if ((msg.from.username || "").toLowerCase() === cleanUsername) { - return msg.chat.id; - } - } - return null; -} - -async function sendTelegramMessage(botToken, text, chatId = null) { - if (!chatId) { - const data = await fetchJson(`https://api.telegram.org/bot${botToken}/getUpdates?limit=1`); - if (!data.ok || !data.result || data.result.length === 0) return; - chatId = data.result[0].message?.chat?.id; - } - if (!chatId) return; - - await fetchJson(`https://api.telegram.org/bot${botToken}/sendMessage`, { - method: "POST", - body: JSON.stringify({ chat_id: chatId, text }), - }); -} - -function fetchJson(url, opts = {}) { - return new Promise((resolve, reject) => { - const req = https.request(url, { - method: opts.method || "GET", - headers: opts.body ? { "Content-Type": "application/json" } : {}, - timeout: 10000, - }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => { - try { - resolve(JSON.parse(body)); - } catch { - resolve({ ok: false }); - } - }); - }); - req.on("error", reject); - if (opts.body) req.write(opts.body); - req.end(); - }); -} - -module.exports = { autoOnboard }; diff --git a/archive/deploy/hermes-railway/src/server.js b/archive/deploy/hermes-railway/src/server.js deleted file mode 100644 index 634f3b9..0000000 --- a/archive/deploy/hermes-railway/src/server.js +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Railway entrypoint — health check + reverse proxy to Hermes dashboard. - * - * Flow: - * 1. Run bootstrap (write .env + config.yaml, sync workspace defaults) - * 2. Auto-onboard if Telegram credentials present - * 3. Start `hermes dashboard` as a child process on the internal port - * 4. Serve health checks + Nunchi /api/* + proxy everything else to dashboard - */ -const express = require("express"); -const crypto = require("crypto"); -const fs = require("fs"); -const path = require("path"); -const { execSync } = require("child_process"); -const httpProxy = require("http-proxy"); -const { bootstrap } = require("./bootstrap.mjs"); -const { startGateway, waitForGatewayReady, getGatewayProcess } = require("./gateway"); -const { autoOnboard } = require("./onboard"); -const { readStatus, readStrategies } = require("./status"); - -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 || "9119", 10); -const START_TIME = Date.now(); -const AGENT_CLI_DIR = "/agent-cli"; -const DATA_DIR = process.env.DATA_DIR || "/data"; -const CONTROL_API_TOKEN = process.env.CONTROL_API_TOKEN || ""; - -const proxy = httpProxy.createProxyServer({ - target: `http://${GATEWAY_HOST}:${GATEWAY_PORT}`, - ws: true, - changeOrigin: true, -}); - -proxy.on("error", (err, req, res) => { - if (res.writeHead) { - res.writeHead(502, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "gateway_unavailable", message: err.message })); - } -}); - -app.get("/health", (req, res) => { - const gw = getGatewayProcess(); - res.json({ - status: "ok", - uptime_s: Math.floor((Date.now() - START_TIME) / 1000), - gateway_alive: gw ? !gw.killed : false, - gateway_pid: gw ? gw.pid : null, - }); -}); - -const CORS_ORIGIN = process.env.CORS_ORIGIN || "*"; -app.use("/api", (req, res, next) => { - res.header("Access-Control-Allow-Origin", CORS_ORIGIN); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Token"); - if (req.method === "OPTIONS") return res.sendStatus(204); - next(); -}); - -function tokensEqual(a, b) { - const left = Buffer.from(a || ""); - const right = Buffer.from(b || ""); - return left.length === right.length && crypto.timingSafeEqual(left, right); -} - -function requireControlAuth(req, res, next) { - if (!CONTROL_API_TOKEN) { - return res.status(503).json({ - error: "control_auth_required", - message: "Set CONTROL_API_TOKEN to enable mutating control endpoints.", - }); - } - - const auth = req.get("authorization") || ""; - const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : ""; - const provided = bearer || req.get("x-api-token") || ""; - if (!tokensEqual(provided, CONTROL_API_TOKEN)) { - return res.status(401).json({ error: "unauthorized" }); - } - return next(); -} - -app.get("/status", async (req, res) => { - try { - const output = execSync("python3 -m cli.main apex status", { - timeout: 10000, - encoding: "utf-8", - cwd: AGENT_CLI_DIR, - }); - res.type("text/plain").send(output); - } catch (e) { - res.type("text/plain").send(e.stdout || e.stderr || e.message); - } -}); - -app.get("/api/status", (req, res) => { - res.json(readStatus()); -}); - -app.get("/api/strategies", (req, res) => { - res.json(readStrategies()); -}); - -app.get("/api/feed", (req, res) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - Connection: "keep-alive", - }); - - let lastTick = -1; - const interval = setInterval(() => { - try { - const status = readStatus(); - const tick = status.tick_count || 0; - if (tick !== lastTick) { - lastTick = tick; - res.write(`data: ${JSON.stringify(status)}\n\n`); - } - } catch { - /* ignore */ - } - }, 2000); - - req.on("close", () => clearInterval(interval)); -}); - -app.post("/api/skill/install", express.json(), (req, res) => { - try { - const output = execSync("python3 -m cli.api.status_reader strategies", { - timeout: 10000, - encoding: "utf-8", - cwd: AGENT_CLI_DIR, - }); - const data = JSON.parse(output.trim()); - const count = Object.keys(data.strategies || {}).length; - res.json({ installed: true, strategies: count, tools: 13 }); - } catch (e) { - res.status(500).json({ installed: false, error: e.message }); - } -}); - -app.post("/api/pause", requireControlAuth, (req, res) => { - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - try { - process.kill(gw.pid, "SIGSTOP"); - res.json({ status: "paused" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } - } else { - res.status(409).json({ error: "No running agent to pause" }); - } -}); - -app.post("/api/resume", requireControlAuth, (req, res) => { - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - try { - process.kill(gw.pid, "SIGCONT"); - res.json({ status: "resumed" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } - } else { - res.status(409).json({ error: "No paused agent to resume" }); - } -}); - -app.post("/api/configure", requireControlAuth, express.json(), (req, res) => { - const configPath = path.join(DATA_DIR, "apex", "config-override.json"); - try { - fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, JSON.stringify(req.body, null, 2)); - res.json({ status: "ok", applied_at: "next_tick" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } -}); - -app.get("/api/trades", (req, res) => { - const limit = parseInt(req.query.limit || "50", 10); - try { - const output = execSync( - `python3 -m cli.api.status_reader trades --data-dir ${DATA_DIR} --limit ${limit}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -app.get("/api/reflect", (req, res) => { - try { - const output = execSync( - `python3 -m cli.api.status_reader reflect --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -app.get("/api/scanner", (req, res) => { - try { - const output = execSync( - `python3 -m cli.api.status_reader radar --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -app.get("/api/journal", (req, res) => { - const limit = parseInt(req.query.limit || "50", 10); - try { - const output = execSync( - `python3 -m cli.api.status_reader journal --data-dir ${DATA_DIR} --limit ${limit}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// Everything else → Hermes dashboard -app.use((req, res) => { - proxy.web(req, res); -}); - -const server = app.listen(PORT, async () => { - console.log(`[server] Listening on :${PORT}`); - - try { - await bootstrap(); - await autoOnboard(); - startGateway(); - await waitForGatewayReady(); - console.log("[server] Hermes dashboard is ready"); - } catch (err) { - console.error("[server] Startup error:", err.message); - // Keep server up so /health stays green and the user can inspect logs - } -}); - -server.on("upgrade", (req, socket, head) => { - proxy.ws(req, socket, head); -}); - -function shutdown(signal) { - console.log(`[server] ${signal} received, shutting down`); - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - gw.kill("SIGTERM"); - setTimeout(() => { - if (!gw.killed) gw.kill("SIGKILL"); - }, 10000); - } - server.close(() => process.exit(0)); - setTimeout(() => process.exit(1), 15000); -} - -process.on("SIGTERM", () => shutdown("SIGTERM")); -process.on("SIGINT", () => shutdown("SIGINT")); diff --git a/archive/deploy/hermes-railway/src/status.js b/archive/deploy/hermes-railway/src/status.js deleted file mode 100644 index 7d5781a..0000000 --- a/archive/deploy/hermes-railway/src/status.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Status reader — shells out to cli.api.status_reader for agent state. - * Identical contract to the openclaw-railway status module: the underlying - * agent runtime doesn't matter, only agent-cli's view of trading state. - */ -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -const AGENT_CLI_DIR = "/agent-cli"; -const DATA_DIR = process.env.DATA_DIR || "/data"; - -function readStatus() { - try { - const output = execSync( - `python3 -m cli.api.status_reader status --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - return JSON.parse(output.trim()); - } catch (err) { - const apexState = path.join(DATA_DIR, "apex", "state.json"); - if (fs.existsSync(apexState)) { - try { - const state = JSON.parse(fs.readFileSync(apexState, "utf-8")); - const active = (state.slots || []).filter((s) => s.status === "active"); - return { - status: "running", - engine: "apex", - tick_count: state.tick_count || 0, - daily_pnl: state.daily_pnl || 0, - total_pnl: state.total_pnl || 0, - active_slots: active, - positions: active.map((s) => ({ - slot: s.slot_id, - market: s.instrument || "", - side: s.side || "", - size: s.entry_size || 0, - entry: s.entry_price || 0, - roe: s.roe_pct || 0, - phase: s.dsl_phase || 0, - })), - }; - } catch { - // fall through - } - } - return { status: "stopped", error: err.message }; - } -} - -function readStrategies() { - try { - const output = execSync( - `python3 -m cli.api.status_reader strategies`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - return JSON.parse(output.trim()); - } catch (err) { - return { error: err.message }; - } -} - -module.exports = { readStatus, readStrategies }; diff --git a/archive/deploy/hermes-railway/workspace/AGENTS.md b/archive/deploy/hermes-railway/workspace/AGENTS.md deleted file mode 100644 index b8da34e..0000000 --- a/archive/deploy/hermes-railway/workspace/AGENTS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Agent Operating Guidelines - -You are a Nunchi autonomous trading agent on Hyperliquid running inside Hermes Agent. You manage positions, scan for opportunities, and protect capital using the `hl` CLI and the `nunchi_trading` MCP server. - -## Core Rules - -1. **Capital preservation first.** Never risk more than the configured daily loss limit. Always use DSL trailing stops on every position. -2. **Data-driven decisions only.** Never invent market data. If you don't have data, run `hl radar once` or `hl movers once` to get it. -3. **Report all actions.** When you enter or exit a position, tell the user via Telegram with: instrument, direction, size, price, and reason. -4. **Verify before trading.** Before any trade, run `hl account` to check balance and `hl status` to see existing positions. -5. **Run REFLECT after sessions.** After any trading session (or when asked), run `hl reflect run` to analyze performance and learn from mistakes. - -## Trading Workflow - -1. **Scan**: `hl radar once` — find the best setups across all HL perps -2. **Validate**: Check radar score (>170 = actionable), confirm direction aligns with BTC macro -3. **Enter**: `hl trade ` or let APEX handle it: `hl apex run` -4. **Monitor**: `hl status --watch` — track positions and PnL -5. **Exit**: DSL handles exits automatically, or manual: `hl trade ` -6. **Review**: `hl reflect run --since ` — analyze what worked and what didn't - -## APEX Autonomous Mode - -When the user says "start trading" or "run APEX": -```bash -hl apex run --preset default -``` - -APEX manages 2-3 concurrent positions automatically: -- Scans for opportunities every 15 ticks -- Detects emerging movers every tick -- Applies DSL trailing stops to every position -- Exits on conviction collapse, stagnation, or hard stops -- Auto-adjusts parameters based on REFLECT performance reviews - -## Safety - -- Never expose private keys, API keys, or tokens in messages -- Never run `git push`, `rm -rf`, or destructive shell commands -- If a trade fails, report the error and suggest next steps — don't retry blindly -- If daily loss limit is triggered, stop all trading and notify the user - -## Memory - -- Hermes persists session memory under `$HERMES_HOME/memories/` automatically — read recent entries on startup for context -- After each trading session, summarize what happened (entries, exits, PnL, lessons) so the next run starts with context -- Track winning/losing patterns across sessions to improve future decisions diff --git a/archive/deploy/hermes-railway/workspace/BOOTSTRAP.md b/archive/deploy/hermes-railway/workspace/BOOTSTRAP.md deleted file mode 100644 index 524b44e..0000000 --- a/archive/deploy/hermes-railway/workspace/BOOTSTRAP.md +++ /dev/null @@ -1,39 +0,0 @@ -# Bootstrap - -Run these checks on every startup. Execute silently — only report errors or the final status. - -## Startup Sequence - -1. **Check environment:** -```bash -hl setup check -``` -If this fails, report the missing configuration to the user and stop. - -2. **Approve builder fee (idempotent):** -```bash -hl builder approve -``` - -3. **Check account balance:** -```bash -hl account -``` -If balance is 0, tell the user: "Account has no balance. Run `hl setup claim-usdyp` for testnet funds, or deposit USDC for mainnet." - -4. **Check existing positions:** -```bash -hl status -``` - -5. **Check APEX state (if exists):** -```bash -hl apex status -``` - -6. **Report ready:** -Send to user: "Agent ready. Balance: $X. Active positions: N. Say 'start trading' to begin APEX, or ask me to scan for opportunities." - -## On Failure - -If any check fails, report the specific error and suggest a fix. Do not start trading with a broken environment. diff --git a/archive/deploy/hermes-railway/workspace/SOUL.md b/archive/deploy/hermes-railway/workspace/SOUL.md deleted file mode 100644 index d79db94..0000000 --- a/archive/deploy/hermes-railway/workspace/SOUL.md +++ /dev/null @@ -1,17 +0,0 @@ -# Soul - -You are a direct, data-driven trading partner. You don't sugarcoat losses or hype wins. You present facts, show the numbers, and let the user decide. - -## Values - -- **Accuracy over speed.** Wrong data is worse than no data. Always verify before acting. -- **Protect capital.** A trader who survives can always trade tomorrow. Never gamble. -- **Admit uncertainty.** If you don't know, say so. Markets are unpredictable — act accordingly. -- **Keep it brief.** Report results concisely. No fluff. Numbers speak louder than narratives. - -## Communication Style - -- Lead with the result: "Entered ETH-PERP long @ $2,450, size 0.5" — not a paragraph of analysis -- Use tables for multi-item data (positions, radar results, REFLECT metrics) -- Flag warnings prominently: "DAILY LOSS LIMIT at 80% — consider stopping" -- Celebrate wins briefly, analyze losses thoroughly diff --git a/archive/deploy/hermes-railway/workspace/TOOLS.md b/archive/deploy/hermes-railway/workspace/TOOLS.md deleted file mode 100644 index f065eb5..0000000 --- a/archive/deploy/hermes-railway/workspace/TOOLS.md +++ /dev/null @@ -1,61 +0,0 @@ -# Tools - -## MCP Server: nunchi_trading - -The primary tool provider, registered in `config.yaml` under `mcp_servers.nunchi_trading`. Exposes 24 trading tools via Model Context Protocol, including: - -- `account` — Show HL account state (balance, margin, positions) -- `status` — Current positions, PnL, and risk state -- `trade` — Place a single order (instrument, side, size) -- `run_strategy` — Start autonomous strategy trading -- `strategies` — List all 14 available strategies -- `radar_run` — Run opportunity radar across all HL perps -- `apex_status` — Show APEX orchestrator state -- `apex_run` — Start APEX autonomous multi-slot trading -- `reflect_run` — Run REFLECT performance review -- `setup_check` — Validate environment configuration -- `builder_status` — Check builder fee approval status -- `wallet_list` — List available wallets -- `wallet_auto` — Create wallet automatically -- `funding_rates` — Read current funding rates -- `funding_hedge_propose` — Build a CFI v2 funding hedge proposal -- `funding_hedge_backtest` — Run the reference funding hedge backtest -- `funding_hedge_execute` — Execute or dry-run a CFI v2 hedge; requires `confirmed=true` - -## CLI: hl - -All MCP tools are also available as CLI commands. Use the CLI for operations not exposed via MCP: - -```bash -hl apex run [--preset default|conservative|aggressive] [--mainnet] -hl radar once [--mock] -hl movers once [--mock] -hl dsl run -i ETH-PERP [--preset tight] -hl reflect run [--since DATE] -hl house join [--url URL] -``` - -## Hermes Built-in Toolsets - -The trading MCP runs alongside Hermes's bundled toolsets. Useful ones: -- `terminal` — run shell commands (e.g. `hl ...`) -- `file` — read/write files in the workspace -- `web` — search and extract live market context -- `cronjob` — schedule recurring tasks -- `skills` — list and view installed skills (e.g. trading playbooks) - -Toggle via `platform_toolsets` in `config.yaml`. The Telegram channel ships with the `hermes-telegram` preset by default. - -## Shell - -Available: `python3`, `node`, `git`, `rg` (ripgrep), `curl` -Not available: `jq` (use `python3 -c "import json; ..."` instead) - -## Cron / Scheduling - -APEX has built-in scheduling: -- Daily PnL reset at UTC midnight -- REFLECT performance review every 4 hours -- Auto-parameter adjustment based on REFLECT findings - -For custom schedules, use Hermes's `cronjob` toolset (`cronjob create ...`) or the gateway's cron system. diff --git a/archive/deploy/openclaw-railway/.env.example b/archive/deploy/openclaw-railway/.env.example deleted file mode 100644 index b5e16dc..0000000 --- a/archive/deploy/openclaw-railway/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# Required -HL_PRIVATE_KEY=0x... # Hyperliquid private key -AI_PROVIDER=anthropic # anthropic, openai, gemini, openrouter, blockrun -AI_API_KEY=sk-ant-... # API key for chosen provider (not needed for blockrun) -TELEGRAM_BOT_TOKEN=123456789:AA... # From @BotFather -TELEGRAM_USERNAME=your_username # Your Telegram @username - -# Optional -HL_TESTNET=true # true (default) or false for mainnet -SETUP_PASSWORD= # Password for control UI (recommended) - -# ClawRouter / x402 (when AI_PROVIDER=blockrun) -# No API key needed — pay per LLM call with USDC via x402 micropayments. -# Install ClawRouter: curl -fsSL https://blockrun.ai/ClawRouter-update | bash -# Fund wallet with USDC on Base or Solana. -# BLOCKRUN_WALLET_KEY=0x... # EVM private key for x402 payment signing -# BLOCKRUN_PROXY_PORT=8402 # ClawRouter local proxy port (default: 8402) -# BLOCKRUN_PAYMENT_CHAIN=base # "base" (EVM) or "solana" diff --git a/archive/deploy/openclaw-railway/Dockerfile b/archive/deploy/openclaw-railway/Dockerfile deleted file mode 100644 index 996c23e..0000000 --- a/archive/deploy/openclaw-railway/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# Build OpenClaw from source -FROM node:22-bookworm AS openclaw-build - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - git ca-certificates curl python3 make g++ \ - && rm -rf /var/lib/apt/lists/* - -RUN curl -fsSL https://bun.sh/install | bash -ENV PATH="/root/.bun/bin:${PATH}" -RUN corepack enable - -WORKDIR /openclaw -ARG OPENCLAW_GIT_REF=v2026.2.22 -RUN git clone --depth 1 --branch "${OPENCLAW_GIT_REF}" https://github.com/openclaw/openclaw.git . - -# Patch workspace protocol references -RUN set -eux; \ - find ./extensions -name 'package.json' -type f | while read -r f; do \ - sed -i -E 's/"openclaw"[[:space:]]*:[[:space:]]*">=[^"]+"/"openclaw": "*"/g' "$f"; \ - sed -i -E 's/"openclaw"[[:space:]]*:[[:space:]]*"workspace:[^"]+"/"openclaw": "*"/g' "$f"; \ - done - -RUN pnpm install --no-frozen-lockfile -RUN pnpm build -ENV OPENCLAW_PREFER_PNPM=1 -RUN pnpm ui:install && pnpm ui:build - - -# Runtime image -FROM node:22-bookworm -ENV NODE_ENV=production - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates curl build-essential gcc g++ make procps file git \ - python3 python3-pip python3-venv pkg-config sudo ripgrep \ - && rm -rf /var/lib/apt/lists/* \ - && ln -sf /usr/bin/python3 /usr/local/bin/python - -# Node tools -RUN corepack enable -RUN npm install -g mcporter@0.7.3 mcp-remote@0.1.38 - -# OpenClaw from build stage -COPY --from=openclaw-build /openclaw /openclaw -RUN printf '%s\n' '#!/usr/bin/env bash' 'exec node /openclaw/dist/entry.js "$@"' \ - > /usr/local/bin/openclaw && chmod +x /usr/local/bin/openclaw - -# rg wrapper: strip grep-style flags that agents pass -RUN mv /usr/bin/rg /usr/bin/rg-real \ - && printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'args=()' \ - 'for a in "$@"; do' \ - ' case "$a" in -R|-r) ;; *) args+=("$a") ;; esac' \ - 'done' \ - 'exec /usr/bin/rg-real "${args[@]}"' \ - > /usr/local/bin/rg && chmod +x /usr/local/bin/rg - -# Install agent-cli (our trading CLI) -# NOTE: build context MUST be the repo root (see deploy/openclaw-railway/railway.toml -# dockerfilePath + Railway service Root Directory = repo root). All COPY paths below -# are therefore relative to the repo root, not to deploy/openclaw-railway/. -WORKDIR /agent-cli -COPY . . -RUN pip install --no-cache-dir --break-system-packages -e ".[mcp]" - -# Wrapper app -WORKDIR /app -COPY deploy/openclaw-railway/package.json ./ -RUN npm install --production - -COPY deploy/openclaw-railway/src ./src - -# Workspace defaults (copied to volume at runtime) -COPY deploy/openclaw-railway/workspace /opt/workspace-defaults - -# Vendor mcporter skill -RUN set -eux; \ - mkdir -p /opt/openclaw-skills; \ - cp -r /openclaw/skills/mcporter /opt/openclaw-skills/ 2>/dev/null || true - -RUN mkdir -p /data -ENV PORT=8080 -EXPOSE 8080 -CMD ["node", "src/server.js"] diff --git a/archive/deploy/openclaw-railway/package.json b/archive/deploy/openclaw-railway/package.json deleted file mode 100644 index ae71171..0000000 --- a/archive/deploy/openclaw-railway/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "nunchi-openclaw-railway", - "version": "0.1.0", - "description": "One-click OpenClaw trading agent for Hyperliquid — powered by Nunchi agent-cli", - "private": true, - "scripts": { - "start": "node src/server.js" - }, - "dependencies": { - "express": "^4.21.0", - "http-proxy": "^1.18.1" - } -} diff --git a/archive/deploy/openclaw-railway/railway.toml b/archive/deploy/openclaw-railway/railway.toml deleted file mode 100644 index 50d7479..0000000 --- a/archive/deploy/openclaw-railway/railway.toml +++ /dev/null @@ -1,23 +0,0 @@ -[build] -builder = "dockerfile" -# Build context is the repo root so the Dockerfile can COPY the agent-cli source. -# Set this service's Root Directory to the repo root (/) in Railway, not -# archive/deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package. -dockerfilePath = "archive/deploy/openclaw-railway/Dockerfile" - -[deploy] -healthcheckPath = "/health" -healthcheckTimeout = 300 -restartPolicyType = "on_failure" - -[[deploy.volumes]] -mountPath = "/data" - -[variables] -PORT = "8080" -HL_TESTNET = "true" -INTERNAL_GATEWAY_HOST = "127.0.0.1" -INTERNAL_GATEWAY_PORT = "18789" -OPENCLAW_ENTRY = "/openclaw/dist/entry.js" -OPENCLAW_STATE_DIR = "/data/.openclaw" -OPENCLAW_WORKSPACE_DIR = "/data/workspace" diff --git a/archive/deploy/openclaw-railway/src/bootstrap.mjs b/archive/deploy/openclaw-railway/src/bootstrap.mjs deleted file mode 100644 index 95275e5..0000000 --- a/archive/deploy/openclaw-railway/src/bootstrap.mjs +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Bootstrap — auto-configure OpenClaw (v2026.2.22) with the Nunchi trading MCP server. - * - * Creates persistent directories, syncs workspace files, writes a v2026.2.22-VALID - * openclaw.json, and registers our stdio MCP server with mcporter (the MCP runtime - * OpenClaw v2026.2.22 ships with). - * - * Schema source: openclaw v2026.2.22 (pinned tag), src/config/zod-schema*.ts. - * The root config schema is `.strict()` (rejects unknown keys), composed in - * src/config/zod-schema.ts -> OpenClawSchema. Every key emitted below maps to a - * real schema field; see the inline citations. - */ -import { existsSync, mkdirSync, copyFileSync, writeFileSync, readdirSync } from "fs"; -import { join } from "path"; -import { execSync } from "child_process"; - -const STATE_DIR = process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"; -const WORKSPACE_DIR = process.env.OPENCLAW_WORKSPACE_DIR || "/data/workspace"; -const WORKSPACE_DEFAULTS = "/opt/workspace-defaults"; -const CONFIG_PATH = join(STATE_DIR, "openclaw.json"); - -// mcporter reads exactly one config file when MCPORTER_CONFIG is set (no home/project -// merge), so we pin an absolute path under the persistent state dir. This makes the -// `nunchi_trading` server reachable from `mcporter` regardless of the agent's cwd. -// Source: mcporter@0.7.3 dist/config.js resolveConfigPath() — explicit --config, then -// process.env.MCPORTER_CONFIG, then /config/mcporter.json. docs/config.md -// "Config Resolution Order" #2: "If MCPORTER_CONFIG is set, only that file is used." -const MCPORTER_CONFIG_PATH = join(STATE_DIR, "mcporter.json"); - -// AI_PROVIDER (our deploy env) -> { envVar, modelRef } for OpenClaw v2026.2.22. -// -// provider key home: OpenClaw resolves provider API keys from provider-native ENV vars -// at runtime (src/agents/live-auth-keys.ts PROVIDER_API_KEY_CONFIG: anthropic -> -// ANTHROPIC_API_KEY, google -> GEMINI_API_KEY (+GOOGLE_API_KEY fallback), openai -> -// OPENAI_API_KEY; derived `${BASE}_API_KEY` for the rest; cf. src/config/io.ts which -// allowlists OPENAI_API_KEY/ANTHROPIC_API_KEY/GEMINI_API_KEY/OPENROUTER_API_KEY). There -// is NO top-level `apiKey`/`provider` key in OpenClawSchema, so the credential is passed -// via env (gateway.js forwards `...process.env` to the gateway child), NOT written to -// openclaw.json. -// -// model home: agents.defaults.model.primary (src/config/zod-schema.agent-defaults.ts -// AgentDefaultsSchema.model.primary: z.string()). Values are OpenClaw's own catalog -// defaults so the active provider matches the supplied key: -// - anthropic alias "sonnet" -> "anthropic/claude-sonnet-4-6" (defaults.ts DEFAULT_MODEL_ALIASES) -// - openai "gpt" -> "openai/gpt-5.2" -// - google/gemini -> "google/gemini-3-pro-preview" -// - openrouter -> "openrouter/auto" (src/commands/onboard-auth.credentials.ts OPENROUTER_DEFAULT_MODEL_REF) -// If model.primary is omitted, OpenClaw falls back to DEFAULT_MODEL="claude-opus-4-6" -// (src/agents/defaults.ts) which is anthropic-only — wrong for non-anthropic keys — -// hence we always set it. -const PROVIDER_MAP = { - anthropic: { envVar: "ANTHROPIC_API_KEY", modelRef: "anthropic/claude-sonnet-4-6" }, - openai: { envVar: "OPENAI_API_KEY", modelRef: "openai/gpt-5.2" }, - gemini: { envVar: "GEMINI_API_KEY", modelRef: "google/gemini-3-pro-preview" }, - google: { envVar: "GEMINI_API_KEY", modelRef: "google/gemini-3-pro-preview" }, - openrouter: { envVar: "OPENROUTER_API_KEY", modelRef: "openrouter/auto" }, -}; - -export async function bootstrap() { - console.log("[bootstrap] Starting auto-configuration..."); - - // 1. Create persistent directories - for (const dir of [ - STATE_DIR, - WORKSPACE_DIR, - join(STATE_DIR, "config"), - join(WORKSPACE_DIR, "memory"), - join(WORKSPACE_DIR, "skills"), - ]) { - mkdirSync(dir, { recursive: true }); - } - - // 2. Sync workspace files from defaults (don't overwrite existing) - if (existsSync(WORKSPACE_DEFAULTS)) { - for (const file of readdirSync(WORKSPACE_DEFAULTS)) { - const dest = join(WORKSPACE_DIR, file); - if (!existsSync(dest)) { - copyFileSync(join(WORKSPACE_DEFAULTS, file), dest); - console.log(`[bootstrap] Synced ${file} to workspace`); - } - } - } - - // 3. Make the AI provider key visible to OpenClaw under the env var its runtime - // resolver expects. Our deploy passes the generic AI_API_KEY; OpenClaw looks for - // the provider-native var (ANTHROPIC_API_KEY, etc.). Set it on this process so it - // propagates to the gateway child (gateway.js spawns with `...process.env`). - // Source: src/agents/live-auth-keys.ts collectProviderApiKeys(). - applyProviderKeyEnv(); - - // 4. Register the nunchi_trading stdio MCP server with mcporter, and pin - // MCPORTER_CONFIG so the agent's `mcporter` calls resolve it. - writeMcporterConfig(); - - // 5. Generate a v2026.2.22-valid openclaw.json - const config = buildConfig(); - writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); - console.log("[bootstrap] Generated openclaw.json"); - - // 6. Auto-approve builder fee (best-effort) - if (process.env.HL_PRIVATE_KEY) { - try { - const mainnet = (process.env.HL_TESTNET || "true").toLowerCase() === "false"; - const args = mainnet ? ["builder", "approve", "--mainnet"] : ["builder", "approve"]; - execSync(`python3 -m cli.main ${args.join(" ")}`, { - timeout: 30000, - cwd: "/agent-cli", - stdio: "pipe", - }); - console.log("[bootstrap] Builder fee approval sent"); - } catch { - // best-effort - } - } - - console.log("[bootstrap] Configuration complete"); -} - -/** Resolve the deploy's AI_PROVIDER -> provider entry (defaults to anthropic). */ -function resolveProvider() { - const aiProvider = (process.env.AI_PROVIDER || "anthropic").toLowerCase(); - const providerInfo = PROVIDER_MAP[aiProvider]; - if (!providerInfo) { - // blockrun/x402/ClawRouter is NOT a provider in OpenClaw v2026.2.22 (no source - // support: grep of the pinned tree for "blockrun"/"x402"/"ClawRouter" is empty). - // Do not fabricate config for it; fall back to anthropic so the gateway still boots. - console.warn( - `[bootstrap] AI_PROVIDER="${aiProvider}" is not supported by OpenClaw v2026.2.22; ` + - "falling back to anthropic. (blockrun/x402 has no native OpenClaw provider.)", - ); - return { aiProvider: "anthropic", providerInfo: PROVIDER_MAP.anthropic }; - } - return { aiProvider, providerInfo }; -} - -/** Export the provider-native API-key env var (from generic AI_API_KEY) for the gateway. */ -function applyProviderKeyEnv() { - const { providerInfo } = resolveProvider(); - const aiKey = process.env.AI_API_KEY || ""; - if (!aiKey) { - console.warn("[bootstrap] AI_API_KEY not set; OpenClaw will have no provider credential"); - return; - } - // Only set if the provider-native var isn't already provided explicitly. - if (!process.env[providerInfo.envVar]?.trim()) { - process.env[providerInfo.envVar] = aiKey; - console.log(`[bootstrap] Exported ${providerInfo.envVar} for OpenClaw provider auth`); - } -} - -/** - * Write mcporter's config registering the nunchi_trading stdio server, and pin - * MCPORTER_CONFIG to it. mcporter config shape (mcporter@0.7.3 dist/config-schema.js - * RawConfigSchema): { mcpServers: Record }> }. `mcpServers` is required even if empty. - */ -function writeMcporterConfig() { - const isMainnet = (process.env.HL_TESTNET || "true").toLowerCase() === "false"; - const mcporterConfig = { - mcpServers: { - nunchi_trading: { - // stdio server: our trading CLI's MCP entrypoint. Array args avoid shell quoting. - command: "python3", - args: ["-m", "cli.main", "mcp", "serve"], - env: { - // Static env for the spawned server. mcporter supports ${VAR} interpolation, - // but we resolve at write-time from the deploy env for determinism. - HL_PRIVATE_KEY: process.env.HL_PRIVATE_KEY || "", - HL_TESTNET: isMainnet ? "false" : "true", - // The MCP server runs from the trading CLI package dir. - PYTHONPATH: "/agent-cli", - }, - }, - }, - }; - writeFileSync(MCPORTER_CONFIG_PATH, JSON.stringify(mcporterConfig, null, 2)); - // Pin for every downstream `mcporter` invocation (forwarded to the gateway child via - // `...process.env` in gateway.js). docs/config.md: MCPORTER_CONFIG => single-file mode. - process.env.MCPORTER_CONFIG = MCPORTER_CONFIG_PATH; - console.log(`[bootstrap] Registered nunchi_trading MCP server (${MCPORTER_CONFIG_PATH})`); -} - -/** - * Build a v2026.2.22-valid openclaw.json. - * - * Root keys used (all present in OpenClawSchema, src/config/zod-schema.ts): - * - gateway.controlUi.{allowInsecureAuth,dangerouslyDisableDeviceAuth} (zod-schema.ts:423-424) - * - agents.defaults.{maxConcurrent,subagents.maxConcurrent,workspace,model.primary} - * (zod-schema.agent-defaults.ts: AgentDefaultsSchema) - * - channels.telegram.{botToken,dmPolicy,allowFrom} (zod-schema.providers-core.ts) - * - * Removed dead keys (rejected by the strict schema) and where they went: - * deviceAuth -> gateway.controlUi.dangerouslyDisableDeviceAuth - * insecureAuth -> gateway.controlUi.allowInsecureAuth - * agentConcurrency -> agents.defaults.maxConcurrent - * subagentConcurrency -> agents.defaults.subagents.maxConcurrent - * provider, apiKey -> dropped (provider-native ENV var + agents.defaults.model.primary) - * mcpServers -> dropped (mcporter.json + MCPORTER_CONFIG, see writeMcporterConfig) - * workspaceDir -> agents.defaults.workspace - * stateDir -> dropped (OPENCLAW_STATE_DIR env, set by gateway.js) - */ -function buildConfig() { - const { providerInfo } = resolveProvider(); - - // Gateway port (deploy passes --port too via gateway.js; keep config consistent). - const gatewayPort = Number.parseInt(process.env.INTERNAL_GATEWAY_PORT || "18789", 10); - - const config = { - gateway: { - // mode=local is REQUIRED to start the gateway: src/cli/gateway-cli/run.ts:212 - // blocks start unless gateway.mode==="local" (or --allow-unconfigured). We set it - // here so the gateway boots from bootstrap config alone, independent of the - // (best-effort, may-fail) `openclaw onboard` step in onboard.js. - mode: "local", - // Loopback bind — the wrapper (server.js) reverse-proxies to it; not exposed directly. - bind: "loopback", - port: gatewayPort, - // Headless deployment: skip the Control UI device-identity + secure-context auth. - // dangerouslyDisableDeviceAuth is the flag that actually disables device-identity - // checks (src/security/audit.ts:405); allowInsecureAuth permits non-HTTPS control - // contexts. Both are under gateway.controlUi (OpenClawSchema gateway.controlUi). - controlUi: { - allowInsecureAuth: true, - dangerouslyDisableDeviceAuth: true, - }, - }, - - agents: { - defaults: { - // Concurrency (was agentConcurrency / subagentConcurrency). - // src/config/agent-limits.ts reads agents.defaults.maxConcurrent and - // agents.defaults.subagents.maxConcurrent. - maxConcurrent: 10, - subagents: { - maxConcurrent: 12, - }, - // Workspace (was workspaceDir). AgentDefaultsSchema.workspace: z.string(). - workspace: WORKSPACE_DIR, - // Provider/model selection. AgentDefaultsSchema.model.primary: z.string(). - model: { - primary: providerInfo.modelRef, - }, - }, - }, - }; - // NOTE on the mcporter skill: we intentionally write NO top-level `skills` config. - // The mcporter skill is BUNDLED with OpenClaw (/openclaw/skills/mcporter/SKILL.md) and - // bundled skills are auto-included by default — src/agents/skills/config.ts - // shouldIncludeSkill() only excludes a skill if skills.entries..enabled===false or - // a non-empty skills.allowBundled allowlist omits it. With `skills` unset, the mcporter - // skill loads automatically as long as its `mcporter` binary exists (it does: Dockerfile - // `npm install -g mcporter@0.7.3`), per the skill's `requires.bins:["mcporter"]` - // eligibility check. Adding `skills.allowBundled:["mcporter"]` would instead DISABLE every - // other bundled skill, so we leave it unset. - - // Telegram integration. Channel config lives under channels.telegram - // (TelegramConfigSchema, zod-schema.providers-core.ts): - // - botToken: z.string() (line 138) - // - allowFrom: (string|number)[] (NOT "allowedUsers") (line 142) - // - dmPolicy: "pairing"|"allowlist"|"open"|"disabled" (DmPolicySchema, default "pairing") - // - // IMPORTANT: Telegram authorization matches on NUMERIC sender/chat IDs, not @usernames - // (confirmed by OpenClaw's own doctor: "Telegram allowFrom contains non-numeric entries - // ...requires numeric sender IDs"). TELEGRAM_USERNAME is a @handle, which is NOT a valid - // allowFrom entry. So: - // - If TELEGRAM_USERNAME is numeric (a chat ID), allowlist it directly. - // - Otherwise leave dmPolicy at its schema default ("pairing") and omit allowFrom; the - // user authorizes via the Telegram pairing flow. (onboard.js separately resolves the - // @handle -> numeric chat ID via getUpdates and records it in USER.md.) - if (process.env.TELEGRAM_BOT_TOKEN) { - const telegram = { - botToken: process.env.TELEGRAM_BOT_TOKEN, - }; - const rawUser = (process.env.TELEGRAM_USERNAME || "").replace("@", "").trim(); - if (rawUser && /^\d+$/.test(rawUser)) { - // Numeric chat ID -> safe to allowlist. - telegram.dmPolicy = "allowlist"; - telegram.allowFrom = [Number(rawUser)]; - } - // else: dmPolicy defaults to "pairing" (omit allowFrom) — do not inject a non-numeric - // @username that would authorize nobody. - config.channels = { telegram }; - } - - return config; -} diff --git a/archive/deploy/openclaw-railway/src/gateway.js b/archive/deploy/openclaw-railway/src/gateway.js deleted file mode 100644 index 50cf266..0000000 --- a/archive/deploy/openclaw-railway/src/gateway.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Gateway lifecycle management — spawn, monitor, and restart the OpenClaw gateway. - */ -const { spawn } = require("child_process"); -const http = require("http"); - -const GATEWAY_HOST = process.env.INTERNAL_GATEWAY_HOST || "127.0.0.1"; -const GATEWAY_PORT = parseInt(process.env.INTERNAL_GATEWAY_PORT || "18789", 10); -const OPENCLAW_ENTRY = process.env.OPENCLAW_ENTRY || "/openclaw/dist/entry.js"; -const STATE_DIR = process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"; - -let gatewayProcess = null; - -function startGateway() { - if (gatewayProcess && !gatewayProcess.killed) { - console.log("[gateway] Already running (pid=%d)", gatewayProcess.pid); - return; - } - - console.log("[gateway] Starting OpenClaw gateway..."); - - gatewayProcess = spawn("node", [OPENCLAW_ENTRY, "gateway", "--port", String(GATEWAY_PORT)], { - env: { - ...process.env, - OPENCLAW_STATE_DIR: STATE_DIR, - OPENCLAW_WORKSPACE_DIR: process.env.OPENCLAW_WORKSPACE_DIR || "/data/workspace", - NODE_ENV: "production", - }, - stdio: ["ignore", "pipe", "pipe"], - }); - - gatewayProcess.stdout.on("data", (data) => { - const line = data.toString().trim(); - if (line) console.log(`[gateway] ${line}`); - }); - - gatewayProcess.stderr.on("data", (data) => { - const line = data.toString().trim(); - // Redact tokens in logs - if (line) console.error(`[gateway] ${redactTokens(line)}`); - }); - - gatewayProcess.on("exit", (code, signal) => { - console.log(`[gateway] Exited (code=${code}, signal=${signal})`); - gatewayProcess = null; - }); - - console.log("[gateway] Spawned (pid=%d)", gatewayProcess.pid); -} - -async function waitForGatewayReady(timeoutMs = 30000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - await httpGet(`http://${GATEWAY_HOST}:${GATEWAY_PORT}/health`); - return true; - } catch { - await sleep(500); - } - } - throw new Error(`Gateway did not become ready within ${timeoutMs}ms`); -} - -function getGatewayProcess() { - return gatewayProcess; -} - -function restartGateway() { - if (gatewayProcess && !gatewayProcess.killed) { - gatewayProcess.kill("SIGTERM"); - setTimeout(() => { - if (gatewayProcess && !gatewayProcess.killed) { - gatewayProcess.kill("SIGKILL"); - } - }, 5000); - } - setTimeout(() => startGateway(), 1500); -} - -// Helpers - -function httpGet(url) { - return new Promise((resolve, reject) => { - http.get(url, { timeout: 3000 }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => resolve(body)); - }).on("error", reject); - }); -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function redactTokens(str) { - return str.replace(/(?:sk-[a-zA-Z0-9-]{10,}|[a-f0-9]{64})/g, "[REDACTED]"); -} - -module.exports = { startGateway, waitForGatewayReady, getGatewayProcess, restartGateway }; diff --git a/archive/deploy/openclaw-railway/src/onboard.js b/archive/deploy/openclaw-railway/src/onboard.js deleted file mode 100644 index 552bfa7..0000000 --- a/archive/deploy/openclaw-railway/src/onboard.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Auto-onboard — configure OpenClaw with AI provider and Telegram on first deploy. - */ -const { execSync } = require("child_process"); -const { existsSync, readFileSync, writeFileSync } = require("fs"); -const { join } = require("path"); -const https = require("https"); - -const STATE_DIR = process.env.OPENCLAW_STATE_DIR || "/data/.openclaw"; -const FINGERPRINT_PATH = join(STATE_DIR, ".env-fingerprint"); - -async function autoOnboard() { - const aiProvider = process.env.AI_PROVIDER; - const aiKey = process.env.AI_API_KEY; - - if (!aiProvider || !aiKey) { - console.log("[onboard] No AI_PROVIDER or AI_API_KEY set, skipping auto-onboard"); - return; - } - - // Check if already configured with same env - const fingerprint = computeFingerprint(); - if (existsSync(FINGERPRINT_PATH)) { - const stored = readFileSync(FINGERPRINT_PATH, "utf-8").trim(); - if (stored === fingerprint) { - console.log("[onboard] Environment unchanged, skipping onboard"); - return; - } - } - - console.log("[onboard] Running auto-onboard..."); - - let telegramChatId = null; - - try { - // Run OpenClaw onboard - execSync("openclaw onboard --non-interactive --accept-risk", { - timeout: 60000, - stdio: "pipe", - env: { - ...process.env, - OPENCLAW_STATE_DIR: STATE_DIR, - }, - }); - console.log("[onboard] OpenClaw onboard complete"); - } catch (err) { - console.warn("[onboard] OpenClaw onboard failed (may already be configured):", err.message); - } - - // Resolve Telegram chat ID if username provided - if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_USERNAME) { - try { - telegramChatId = await resolveTelegramChatId( - process.env.TELEGRAM_BOT_TOKEN, - process.env.TELEGRAM_USERNAME, - ); - if (telegramChatId) { - console.log(`[onboard] Resolved Telegram chat ID: ${telegramChatId}`); - // Write USER.md with chat ID - const userMd = `# User\n\nTelegram chat ID: ${telegramChatId}\nUsername: ${process.env.TELEGRAM_USERNAME}\n`; - writeFileSync( - join(process.env.OPENCLAW_WORKSPACE_DIR || "/data/workspace", "USER.md"), - userMd, - ); - } - } catch (err) { - console.warn("[onboard] Could not resolve Telegram chat ID:", err.message); - } - } - - // Send ready message to Telegram - if (process.env.TELEGRAM_BOT_TOKEN) { - try { - await sendTelegramMessage( - process.env.TELEGRAM_BOT_TOKEN, - "Nunchi trading agent is ready. Say 'hl apex run' to start autonomous trading, or 'hl radar once' to scan for opportunities.", - telegramChatId, - ); - console.log("[onboard] Sent ready message to Telegram"); - } catch (err) { - console.warn("[onboard] Could not send Telegram message:", err.message); - } - } - - // Store fingerprint - writeFileSync(FINGERPRINT_PATH, fingerprint); - console.log("[onboard] Onboarding complete"); -} - -function computeFingerprint() { - const crypto = require("crypto"); - const parts = [ - process.env.AI_PROVIDER || "", - (process.env.AI_API_KEY || "").slice(-8), - process.env.TELEGRAM_BOT_TOKEN ? "tg" : "", - process.env.HL_TESTNET || "true", - ]; - return crypto.createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 16); -} - -async function resolveTelegramChatId(botToken, username) { - const cleanUsername = username.replace("@", "").toLowerCase(); - const data = await fetchJson(`https://api.telegram.org/bot${botToken}/getUpdates?limit=50`); - if (!data.ok || !data.result) return null; - - for (const update of data.result) { - const msg = update.message || update.my_chat_member; - if (!msg || !msg.from) continue; - if ((msg.from.username || "").toLowerCase() === cleanUsername) { - return msg.chat.id; - } - } - return null; -} - -async function sendTelegramMessage(botToken, text, chatId = null) { - if (!chatId) { - // Try to find a chat to send to when no username-specific chat was resolved. - const data = await fetchJson(`https://api.telegram.org/bot${botToken}/getUpdates?limit=1`); - if (!data.ok || !data.result || data.result.length === 0) return; - chatId = data.result[0].message?.chat?.id; - } - if (!chatId) return; - - await fetchJson(`https://api.telegram.org/bot${botToken}/sendMessage`, { - method: "POST", - body: JSON.stringify({ chat_id: chatId, text }), - }); -} - -function fetchJson(url, opts = {}) { - return new Promise((resolve, reject) => { - const req = https.request(url, { - method: opts.method || "GET", - headers: opts.body ? { "Content-Type": "application/json" } : {}, - timeout: 10000, - }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => { - try { - resolve(JSON.parse(body)); - } catch { - resolve({ ok: false }); - } - }); - }); - req.on("error", reject); - if (opts.body) req.write(opts.body); - req.end(); - }); -} - -module.exports = { autoOnboard }; diff --git a/archive/deploy/openclaw-railway/src/server.js b/archive/deploy/openclaw-railway/src/server.js deleted file mode 100644 index bb13b21..0000000 --- a/archive/deploy/openclaw-railway/src/server.js +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Railway entrypoint — health check + reverse proxy to OpenClaw gateway. - * - * Flow: - * 1. Run bootstrap (auto-configure OpenClaw + MCP + Telegram) - * 2. Start OpenClaw gateway as child process - * 3. Serve health checks + proxy all other traffic to gateway - */ -const express = require("express"); -const crypto = require("crypto"); -const fs = require("fs"); -const path = require("path"); -const { execSync } = require("child_process"); -const httpProxy = require("http-proxy"); -const { bootstrap } = require("./bootstrap.mjs"); -const { startGateway, waitForGatewayReady, getGatewayProcess } = require("./gateway"); -const { autoOnboard } = require("./onboard"); -const { readStatus, readStrategies } = require("./status"); - -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 START_TIME = Date.now(); -const AGENT_CLI_DIR = "/agent-cli"; -const DATA_DIR = process.env.DATA_DIR || "/data"; -const CONTROL_API_TOKEN = process.env.CONTROL_API_TOKEN || ""; - -// Proxy to OpenClaw gateway -const proxy = httpProxy.createProxyServer({ - target: `http://${GATEWAY_HOST}:${GATEWAY_PORT}`, - ws: true, - changeOrigin: true, -}); - -proxy.on("error", (err, req, res) => { - if (res.writeHead) { - res.writeHead(502, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "gateway_unavailable", message: err.message })); - } -}); - -// Health check -app.get("/health", (req, res) => { - const gw = getGatewayProcess(); - res.json({ - status: "ok", - uptime_s: Math.floor((Date.now() - START_TIME) / 1000), - gateway_alive: gw ? !gw.killed : false, - gateway_pid: gw ? gw.pid : null, - }); -}); - -// CORS middleware for /api/* routes -const CORS_ORIGIN = process.env.CORS_ORIGIN || "*"; -app.use("/api", (req, res, next) => { - res.header("Access-Control-Allow-Origin", CORS_ORIGIN); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Token"); - if (req.method === "OPTIONS") return res.sendStatus(204); - next(); -}); - -function tokensEqual(a, b) { - const left = Buffer.from(a || ""); - const right = Buffer.from(b || ""); - return left.length === right.length && crypto.timingSafeEqual(left, right); -} - -function requireControlAuth(req, res, next) { - if (!CONTROL_API_TOKEN) { - return res.status(503).json({ - error: "control_auth_required", - message: "Set CONTROL_API_TOKEN to enable mutating control endpoints.", - }); - } - - const auth = req.get("authorization") || ""; - const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : ""; - const provided = bearer || req.get("x-api-token") || ""; - if (!tokensEqual(provided, CONTROL_API_TOKEN)) { - return res.status(401).json({ error: "unauthorized" }); - } - return next(); -} - -// Trading status (human-readable, calls hl CLI directly) -app.get("/status", async (req, res) => { - const { execSync } = require("child_process"); - try { - const output = execSync("python3 -m cli.main apex status", { - timeout: 10000, - encoding: "utf-8", - cwd: "/agent-cli", - }); - res.type("text/plain").send(output); - } catch (e) { - res.type("text/plain").send(e.stdout || e.stderr || e.message); - } -}); - -// API: Agent status (JSON, for UI) -app.get("/api/status", (req, res) => { - res.json(readStatus()); -}); - -// API: Strategy catalog -app.get("/api/strategies", (req, res) => { - res.json(readStrategies()); -}); - -// API: SSE feed — polls status every 2s -app.get("/api/feed", (req, res) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - Connection: "keep-alive", - }); - - let lastTick = -1; - const interval = setInterval(() => { - try { - const status = readStatus(); - const tick = status.tick_count || 0; - if (tick !== lastTick) { - lastTick = tick; - res.write(`data: ${JSON.stringify(status)}\n\n`); - } - } catch { - // ignore read errors - } - }, 2000); - - req.on("close", () => clearInterval(interval)); -}); - -// API: Install/update Nunchi trading skill -app.post("/api/skill/install", express.json(), (req, res) => { - const { execSync } = require("child_process"); - try { - // Verify agent-cli is available by checking strategies - const output = execSync("python3 -m cli.api.status_reader strategies", { - timeout: 10000, - encoding: "utf-8", - cwd: "/agent-cli", - }); - const data = JSON.parse(output.trim()); - const count = Object.keys(data.strategies || {}).length; - res.json({ installed: true, strategies: count, tools: 13 }); - } catch (e) { - res.status(500).json({ installed: false, error: e.message }); - } -}); - -// API: Pause agent -app.post("/api/pause", requireControlAuth, (req, res) => { - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - try { - process.kill(gw.pid, "SIGSTOP"); - res.json({ status: "paused" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } - } else { - res.status(409).json({ error: "No running agent to pause" }); - } -}); - -// API: Resume agent -app.post("/api/resume", requireControlAuth, (req, res) => { - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - try { - process.kill(gw.pid, "SIGCONT"); - res.json({ status: "resumed" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } - } else { - res.status(409).json({ error: "No paused agent to resume" }); - } -}); - -// API: Configure agent (write config override) -app.post("/api/configure", requireControlAuth, express.json(), (req, res) => { - const configPath = path.join(DATA_DIR, "apex", "config-override.json"); - try { - fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, JSON.stringify(req.body, null, 2)); - res.json({ status: "ok", applied_at: "next_tick" }); - } catch (e) { - res.status(500).json({ error: e.message }); - } -}); - -// API: Trade history -app.get("/api/trades", (req, res) => { - const limit = parseInt(req.query.limit || "50", 10); - try { - const output = execSync( - `python3 -m cli.api.status_reader trades --data-dir ${DATA_DIR} --limit ${limit}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// API: REFLECT reports -app.get("/api/reflect", (req, res) => { - try { - const output = execSync( - `python3 -m cli.api.status_reader reflect --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// API: RADAR (scanner) history -app.get("/api/scanner", (req, res) => { - try { - const output = execSync( - `python3 -m cli.api.status_reader radar --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// API: Journal entries -app.get("/api/journal", (req, res) => { - const limit = parseInt(req.query.limit || "50", 10); - try { - const output = execSync( - `python3 -m cli.api.status_reader journal --data-dir ${DATA_DIR} --limit ${limit}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - res.json(JSON.parse(output.trim())); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// Everything else proxies to OpenClaw gateway -app.use((req, res) => { - proxy.web(req, res); -}); - -// WebSocket upgrade -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 2: Auto-onboard if credentials present - await autoOnboard(); - - // Step 3: Start OpenClaw gateway - startGateway(); - await waitForGatewayReady(); - console.log("[server] OpenClaw gateway is ready"); - } catch (err) { - console.error("[server] Startup error:", err.message); - // Keep server running for health checks even if gateway fails - } -}); - -server.on("upgrade", (req, socket, head) => { - proxy.ws(req, socket, head); -}); - -// Graceful shutdown -function shutdown(signal) { - console.log(`[server] ${signal} received, shutting down`); - const gw = getGatewayProcess(); - if (gw && !gw.killed) { - gw.kill("SIGTERM"); - setTimeout(() => { - if (!gw.killed) gw.kill("SIGKILL"); - }, 10000); - } - server.close(() => process.exit(0)); - setTimeout(() => process.exit(1), 15000); -} - -process.on("SIGTERM", () => shutdown("SIGTERM")); -process.on("SIGINT", () => shutdown("SIGINT")); diff --git a/archive/deploy/openclaw-railway/src/status.js b/archive/deploy/openclaw-railway/src/status.js deleted file mode 100644 index 1a920e9..0000000 --- a/archive/deploy/openclaw-railway/src/status.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Status reader — shells out to cli.api.status_reader for agent state. - */ -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -const AGENT_CLI_DIR = "/agent-cli"; -const DATA_DIR = process.env.DATA_DIR || "/data"; - -function readStatus() { - try { - const output = execSync( - `python3 -m cli.api.status_reader status --data-dir ${DATA_DIR}`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - return JSON.parse(output.trim()); - } catch (err) { - // Fallback: try reading apex state.json directly - const apexState = path.join(DATA_DIR, "apex", "state.json"); - if (fs.existsSync(apexState)) { - try { - const state = JSON.parse(fs.readFileSync(apexState, "utf-8")); - const active = (state.slots || []).filter((s) => s.status === "active"); - return { - status: "running", - engine: "apex", - tick_count: state.tick_count || 0, - daily_pnl: state.daily_pnl || 0, - total_pnl: state.total_pnl || 0, - active_slots: active, - positions: active.map((s) => ({ - slot: s.slot_id, - market: s.instrument || "", - side: s.side || "", - size: s.entry_size || 0, - entry: s.entry_price || 0, - roe: s.roe_pct || 0, - phase: s.dsl_phase || 0, - })), - }; - } catch { - // fall through - } - } - return { status: "stopped", error: err.message }; - } -} - -function readStrategies() { - try { - const output = execSync( - `python3 -m cli.api.status_reader strategies`, - { timeout: 10000, encoding: "utf-8", cwd: AGENT_CLI_DIR, stdio: ["pipe", "pipe", "pipe"] } - ); - return JSON.parse(output.trim()); - } catch (err) { - return { error: err.message }; - } -} - -module.exports = { readStatus, readStrategies }; diff --git a/archive/deploy/openclaw-railway/workspace/AGENTS.md b/archive/deploy/openclaw-railway/workspace/AGENTS.md deleted file mode 100644 index cfff725..0000000 --- a/archive/deploy/openclaw-railway/workspace/AGENTS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Agent Operating Guidelines - -You are a Nunchi autonomous trading agent on Hyperliquid. You manage positions, scan for opportunities, and protect capital using the `hl` CLI and MCP tools. - -## Core Rules - -1. **Capital preservation first.** Never risk more than the configured daily loss limit. Always use DSL trailing stops on every position. -2. **Data-driven decisions only.** Never invent market data. If you don't have data, run `hl radar once` or `hl movers once` to get it. -3. **Report all actions.** When you enter or exit a position, tell the user via Telegram with: instrument, direction, size, price, and reason. -4. **Verify before trading.** Before any trade, run `hl account` to check balance and `hl status` to see existing positions. -5. **Run REFLECT after sessions.** After any trading session (or when asked), run `hl reflect run` to analyze performance and learn from mistakes. - -## Trading Workflow - -1. **Scan**: `hl radar once` — find the best setups across all HL perps -2. **Validate**: Check radar score (>170 = actionable), confirm direction aligns with BTC macro -3. **Enter**: `hl trade ` or let APEX handle it: `hl apex run` -4. **Monitor**: `hl status --watch` — track positions and PnL -5. **Exit**: DSL handles exits automatically, or manual: `hl trade ` -6. **Review**: `hl reflect run --since ` — analyze what worked and what didn't - -## APEX Autonomous Mode - -When the user says "start trading" or "run APEX": -```bash -hl apex run --preset default -``` - -APEX manages 2-3 concurrent positions automatically: -- Scans for opportunities every 15 ticks -- Detects emerging movers every tick -- Applies DSL trailing stops to every position -- Exits on conviction collapse, stagnation, or hard stops -- Auto-adjusts parameters based on REFLECT performance reviews - -## Safety - -- Never expose private keys, API keys, or tokens in messages -- Never run `git push`, `rm -rf`, or destructive shell commands -- If a trade fails, report the error and suggest next steps — don't retry blindly -- If daily loss limit is triggered, stop all trading and notify the user - -## Memory - -- Read `memory/session.md` on startup for context from previous sessions -- After each trading session, write a brief summary to `memory/session.md` -- Track winning/losing patterns to improve future decisions diff --git a/archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md b/archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md deleted file mode 100644 index 524b44e..0000000 --- a/archive/deploy/openclaw-railway/workspace/BOOTSTRAP.md +++ /dev/null @@ -1,39 +0,0 @@ -# Bootstrap - -Run these checks on every startup. Execute silently — only report errors or the final status. - -## Startup Sequence - -1. **Check environment:** -```bash -hl setup check -``` -If this fails, report the missing configuration to the user and stop. - -2. **Approve builder fee (idempotent):** -```bash -hl builder approve -``` - -3. **Check account balance:** -```bash -hl account -``` -If balance is 0, tell the user: "Account has no balance. Run `hl setup claim-usdyp` for testnet funds, or deposit USDC for mainnet." - -4. **Check existing positions:** -```bash -hl status -``` - -5. **Check APEX state (if exists):** -```bash -hl apex status -``` - -6. **Report ready:** -Send to user: "Agent ready. Balance: $X. Active positions: N. Say 'start trading' to begin APEX, or ask me to scan for opportunities." - -## On Failure - -If any check fails, report the specific error and suggest a fix. Do not start trading with a broken environment. diff --git a/archive/deploy/openclaw-railway/workspace/SOUL.md b/archive/deploy/openclaw-railway/workspace/SOUL.md deleted file mode 100644 index d79db94..0000000 --- a/archive/deploy/openclaw-railway/workspace/SOUL.md +++ /dev/null @@ -1,17 +0,0 @@ -# Soul - -You are a direct, data-driven trading partner. You don't sugarcoat losses or hype wins. You present facts, show the numbers, and let the user decide. - -## Values - -- **Accuracy over speed.** Wrong data is worse than no data. Always verify before acting. -- **Protect capital.** A trader who survives can always trade tomorrow. Never gamble. -- **Admit uncertainty.** If you don't know, say so. Markets are unpredictable — act accordingly. -- **Keep it brief.** Report results concisely. No fluff. Numbers speak louder than narratives. - -## Communication Style - -- Lead with the result: "Entered ETH-PERP long @ $2,450, size 0.5" — not a paragraph of analysis -- Use tables for multi-item data (positions, radar results, REFLECT metrics) -- Flag warnings prominently: "DAILY LOSS LIMIT at 80% — consider stopping" -- Celebrate wins briefly, analyze losses thoroughly diff --git a/archive/deploy/openclaw-railway/workspace/TOOLS.md b/archive/deploy/openclaw-railway/workspace/TOOLS.md deleted file mode 100644 index 49964cb..0000000 --- a/archive/deploy/openclaw-railway/workspace/TOOLS.md +++ /dev/null @@ -1,59 +0,0 @@ -# Tools - -## MCP Server: nunchi_trading - -The primary tool provider. Reach it through the **mcporter** skill — it is registered -as an mcporter stdio server (not a native gateway tool). Usage: - -```bash -mcporter list nunchi_trading --schema # list the server's tools + input schemas -mcporter call nunchi_trading.account # call a tool (no args) -mcporter call nunchi_trading.trade instrument=ETH-PERP side=buy size=0.1 -``` - -Exposes 24 trading tools via Model Context Protocol, including: - -- `account` — Show HL account state (balance, margin, positions) -- `status` — Current positions, PnL, and risk state -- `trade` — Place a single order (instrument, side, size) -- `run_strategy` — Start autonomous strategy trading -- `strategies` — List all 14 available strategies -- `radar_run` — Run opportunity radar across all HL perps -- `apex_status` — Show APEX orchestrator state -- `apex_run` — Start APEX autonomous multi-slot trading -- `reflect_run` — Run REFLECT performance review -- `setup_check` — Validate environment configuration -- `builder_status` — Check builder fee approval status -- `wallet_list` — List available wallets -- `wallet_auto` — Create wallet automatically -- `funding_rates` — Read current funding rates -- `funding_hedge_propose` — Build a CFI v2 funding hedge proposal -- `funding_hedge_backtest` — Run the reference funding hedge backtest -- `funding_hedge_execute` — Execute or dry-run a CFI v2 hedge; requires `confirmed=true` - -## CLI: hl - -All MCP tools are also available as CLI commands. Use the CLI for operations not exposed via MCP: - -```bash -hl apex run [--preset default|conservative|aggressive] [--mainnet] -hl radar once [--mock] -hl movers once [--mock] -hl dsl run -i ETH-PERP [--preset tight] -hl reflect run [--since DATE] -hl house join [--url URL] -``` - -## Shell - -Available: `python3`, `node`, `git`, `rg` (ripgrep), `curl` -Not available: `jq` (use `python3 -c "import json; ..."` instead) - -## Cron / Scheduling - -APEX has built-in scheduling: -- Daily PnL reset at UTC midnight -- REFLECT performance review every 4 hours -- Auto-parameter adjustment based on REFLECT findings - -For custom schedules, use the gateway's cron system. diff --git a/cli/api/status_reader.py b/cli/api/status_reader.py index fa017dc..93503bd 100644 --- a/cli/api/status_reader.py +++ b/cli/api/status_reader.py @@ -2,7 +2,6 @@ Shared utility used by: - scripts/entrypoint.py (imported directly) -- archive/deploy/openclaw-railway/src/server.js (via `python3 -m cli.api.status_reader`) """ from __future__ import annotations diff --git a/cli/hedge_display.py b/cli/hedge_display.py index 5e9e267..7a4966d 100644 --- a/cli/hedge_display.py +++ b/cli/hedge_display.py @@ -153,7 +153,7 @@ def hedge_proposal_block(proposal, snapshot, *, mainnet: bool = False) -> str: lines.extend([ "", f"{DIM}Execution: signs against {hedge.market} (HL HIP-3, asset index resolved " - f"by the HL Python SDK). 10 bps Nunchi builder fee. No hermes-api dep.{RESET}", + f"by the HL Python SDK). 10 bps Nunchi builder fee.{RESET}", ]) return "\n".join(lines) diff --git a/cli/skill.md b/cli/skill.md index 4311127..8048415 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -6,13 +6,13 @@ argument-hint: "hedge [coin] | run cfi_hedge" allowed-tools: - Bash metadata: - openclaw: - requires: - env: - - HL_PRIVATE_KEY - bins: - - python3 - primaryEnv: HL_PRIVATE_KEY + author: Nunchi Trade + requires: + env: + - HL_PRIVATE_KEY + bins: + - python3 + primaryEnv: HL_PRIVATE_KEY --- # YEX Trader — CFI funding-rate hedge diff --git a/cli/telemetry.py b/cli/telemetry.py index 47c0dc8..778e71f 100644 --- a/cli/telemetry.py +++ b/cli/telemetry.py @@ -42,10 +42,6 @@ def _get_version() -> str: def _detect_deploy_mode() -> str: if os.environ.get("RAILWAY_SERVICE_NAME"): return "railway" - if os.environ.get("OPENCLAW_STATE_DIR"): - return "openclaw" - if os.environ.get("HERMES_HOME"): - return "hermes" return "local" diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 8d63b7f..5790eda 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -2,7 +2,7 @@ The default Railway deployment is the shared MCP/tools runtime used behind `mcp-gateway`. It should run `RUN_MODE=mcp` and expose tools only; it must not -run a per-user Hermes/OpenClaw/autonomous-agent loop for the subscription +run a per-user autonomous-agent loop for the subscription product. The APEX sections below apply only when an operator explicitly opts into self-hosted autonomous modes. @@ -134,7 +134,6 @@ railway logs | jq '.level, .message' - [ ] `RUN_MODE=mcp` for Nunchi-hosted shared tools runtime - [ ] `HL_TESTNET=true` unless the runner is explicitly approved for mainnet -- [ ] Do not deploy Hermes/OpenClaw as a Nunchi subscription product surface - [ ] Configure generic metering upload when reporting usage: `NUNCHI_METERING_URL`, `NUNCHI_METERING_TOKEN` - [ ] `API_AUTH_TOKEN` set for control endpoint security - [ ] Persistent volume mounted at `/data` diff --git a/docs/api-reference.md b/docs/api-reference.md index 9ae7dd3..b99c268 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -6,7 +6,7 @@ This guide covers every method for pulling data from a running Nunchi agent. Thr |------|----------|----------| | HTTP REST API | HTTP/JSON | Dashboards, monitoring, external integrations | | SSE Feed | Server-Sent Events | Live streaming to frontends | -| MCP Server | Model Context Protocol | AI agent orchestration (Claude, OpenClaw) | +| MCP Server | Model Context Protocol | AI agent orchestration (Claude Code, Cursor, Codex) | > **Security:** All endpoints are unauthenticated. If your agent is publicly accessible, anyone with the URL can read its state. Plan your network security accordingly. @@ -27,14 +27,14 @@ python scripts/entrypoint.py The HTTP server binds to `0.0.0.0:$PORT` (default 8080). -**Railway / OpenClaw (Node.js entrypoint):** +**Railway (shared MCP tools runtime):** ```bash -# Express server with reverse proxy to OpenClaw gateway -node src/server.js +# Python entrypoint — health server on $PORT + MCP SSE server +python scripts/entrypoint.py ``` -The Express server binds to `0.0.0.0:$PORT` (default 8080) and exposes the same API surface. +The HTTP server binds to `0.0.0.0:$PORT` (default 8080) and exposes the REST API surface. ### Base URL @@ -583,7 +583,7 @@ setInterval(() => fetchLeaderboard().then(renderTable), 30000); ## MCP Server -The MCP server exposes 21 tools for AI agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Claude Code, OpenClaw, or any MCP-compatible client. +The MCP server exposes 21 tools for AI agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Claude Code, Cursor, Codex, or any MCP-compatible client. ### Starting the Server diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index feff42a..406d8a7 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -104,10 +104,6 @@ def test_railway(self): with patch.dict(os.environ, {"RAILWAY_SERVICE_NAME": "agent-cli"}): assert _detect_deploy_mode() == "railway" - def test_openclaw(self): - with patch.dict(os.environ, {"OPENCLAW_STATE_DIR": "/data/.openclaw"}): - assert _detect_deploy_mode() == "openclaw" - class TestFactory: def test_create_telemetry_returns_client(self):