diff --git a/README.md b/README.md
index d77b805..353a923 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,10 +18,10 @@
-
+
-
+
@@ -32,7 +32,9 @@
---
-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`. 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) was archived 2026-07-02 under `_archive/` — see `_archive/README.md`. Hermes/OpenClaw deploy templates were removed.
---
@@ -51,8 +53,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 +66,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,8 +79,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 apex run --mainnet
+hl hedge propose BTC
+hl run cfi_hedge -i BTCSWP-PARA --mock --max-ticks 3 --mainnet
```
See [Markets & Instruments](#markets--instruments) for native perps, Paragon BTCSWP, and YEX symbols.
@@ -93,10 +97,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 +118,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,116 +137,51 @@ 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
-
-19 built-in strategies. Every strategy extends `BaseStrategy` with a single `on_tick()` method — no shared state, no hidden coupling.
-
-| 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 |
-
-Run `hl strategies` for the full registry and default parameters.
-
-### 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: CFI funding-rate hedge
-| 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. |
+The product ships **one** registered strategy: **`cfi_hedge`**. Legacy MM/signal strategies were archived on 2026-07-02 under `strategies/_archive/` (see README there).
-### Quoting Engine Pipeline
+| 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 |
-The engine-powered strategies (`engine_mm`, `funding_arb`, `regime_mm`, `liquidation_mm`) share a common pipeline:
+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 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`.
---
## 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)
-
-```bash
-clawhub install nunchi-trade/yex-trader
```
### Install a skill (Claude Code)
@@ -254,7 +195,7 @@ cp ~/agent-cli/cli/skill.md ~/.claude/skills/yex-trader/SKILL.md
---
-## Autonomous Trading Stack
+## Active Operator Surface
### Onboard
@@ -266,247 +207,29 @@ 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)**
---
-### 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.
+### CFI hedge commands
-| 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 |
+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 radar once --mock # Single scan
-hl radar run --mock # Continuous (every 15 min)
+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
```
-**[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.
+### Archived legacy stack
-**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 |
-
-```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
-```
-
-REFLECT auto-generates research directions:
-
-| 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.
---
@@ -514,22 +237,14 @@ 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
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)
@@ -550,16 +265,18 @@ 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.
### 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
@@ -567,8 +284,6 @@ 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.
-
### 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.
@@ -580,7 +295,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.
@@ -592,25 +307,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.
-
-### 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 `deploy/openclaw-railway/railway.toml` or
-`deploy/hermes-railway/railway.toml` as the config-as-code path and keep the
-Railway build root at the repo root.
+- **strategy** — local/self-hosted `cfi_hedge` loop, not the Nunchi subscription product path. Legacy `apex`/`wolf` values are deprecated and fall back to `strategy`.
---
@@ -618,28 +322,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/ 19 trading strategy implementations
-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
+strategies/ cfi_hedge + archived legacy strategies (_archive/)
+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, scripts, docs, and tests
sdk/ Strategy base class and model registry
parent/ HL API proxy, position tracking, risk management
scripts/ Backtest harness, bootstrap
@@ -712,7 +405,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
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/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 96%
rename from tests/test_guard_implementations.py
rename to _archive/tests/test_guard_implementations.py
index 0905d49..afdda92 100644
--- a/tests/test_guard_implementations.py
+++ b/_archive/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_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 52%
rename from tests/test_market_strategy_routing.py
rename to _archive/tests/test_market_strategy_routing.py
index f8067e7..16ee22a 100644
--- a/tests/test_market_strategy_routing.py
+++ b/_archive/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_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..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)
-- 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..e3136cf 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.
"""
@@ -115,9 +115,26 @@ 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 quoting_engine.feeds.seda_oracle import (
- fetch_btcswp_snapshot,
+ from strategies.cfi_funding import (
+ fetch_cfi_funding_snapshot,
fetch_hl_current_funding_hr,
)
@@ -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)
@@ -145,7 +161,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.
@@ -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.
@@ -303,8 +358,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 +382,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"))
@@ -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/commands/run.py b/cli/commands/run.py
index dcd5f5e..49895c0 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",
@@ -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/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/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..7a4966d 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 = [
@@ -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/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 d000ade..f55d5d5 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",
@@ -55,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",
@@ -228,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
@@ -357,9 +363,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."
@@ -488,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:
@@ -590,7 +600,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)
@@ -627,75 +637,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/cli/skill.md b/cli/skill.md
index 7807ca5..8048415 100644
--- a/cli/skill.md
+++ b/cli/skill.md
@@ -1,23 +1,25 @@
---
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:
- 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
+# 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/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/deploy/hermes-railway/Dockerfile b/deploy/hermes-railway/Dockerfile
deleted file mode 100644
index c00f246..0000000
--- a/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/deploy/hermes-railway/package.json b/deploy/hermes-railway/package.json
deleted file mode 100644
index 4bdf4e6..0000000
--- a/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/deploy/hermes-railway/railway.toml b/deploy/hermes-railway/railway.toml
deleted file mode 100644
index a1db35e..0000000
--- a/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
-# deploy/hermes-railway, otherwise `COPY . .` cannot reach the CLI package.
-dockerfilePath = "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/deploy/hermes-railway/src/bootstrap.mjs b/deploy/hermes-railway/src/bootstrap.mjs
deleted file mode 100644
index 7c60579..0000000
--- a/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/deploy/hermes-railway/src/gateway.js b/deploy/hermes-railway/src/gateway.js
deleted file mode 100644
index 95a467b..0000000
--- a/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/deploy/hermes-railway/src/onboard.js b/deploy/hermes-railway/src/onboard.js
deleted file mode 100644
index ef4dd5e..0000000
--- a/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/deploy/hermes-railway/src/server.js b/deploy/hermes-railway/src/server.js
deleted file mode 100644
index 634f3b9..0000000
--- a/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/deploy/hermes-railway/src/status.js b/deploy/hermes-railway/src/status.js
deleted file mode 100644
index 7d5781a..0000000
--- a/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/deploy/hermes-railway/workspace/AGENTS.md b/deploy/hermes-railway/workspace/AGENTS.md
deleted file mode 100644
index b8da34e..0000000
--- a/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/deploy/hermes-railway/workspace/BOOTSTRAP.md b/deploy/hermes-railway/workspace/BOOTSTRAP.md
deleted file mode 100644
index 524b44e..0000000
--- a/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/deploy/hermes-railway/workspace/SOUL.md b/deploy/hermes-railway/workspace/SOUL.md
deleted file mode 100644
index d79db94..0000000
--- a/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/deploy/hermes-railway/workspace/TOOLS.md b/deploy/hermes-railway/workspace/TOOLS.md
deleted file mode 100644
index f065eb5..0000000
--- a/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/deploy/openclaw-railway/.env.example b/deploy/openclaw-railway/.env.example
deleted file mode 100644
index b5e16dc..0000000
--- a/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/deploy/openclaw-railway/Dockerfile b/deploy/openclaw-railway/Dockerfile
deleted file mode 100644
index 996c23e..0000000
--- a/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/deploy/openclaw-railway/package.json b/deploy/openclaw-railway/package.json
deleted file mode 100644
index ae71171..0000000
--- a/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/deploy/openclaw-railway/railway.toml b/deploy/openclaw-railway/railway.toml
deleted file mode 100644
index bd0e575..0000000
--- a/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
-# deploy/openclaw-railway, otherwise `COPY . .` cannot reach the CLI package.
-dockerfilePath = "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/deploy/openclaw-railway/src/bootstrap.mjs b/deploy/openclaw-railway/src/bootstrap.mjs
deleted file mode 100644
index 95275e5..0000000
--- a/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/deploy/openclaw-railway/src/gateway.js b/deploy/openclaw-railway/src/gateway.js
deleted file mode 100644
index 50cf266..0000000
--- a/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/deploy/openclaw-railway/src/onboard.js b/deploy/openclaw-railway/src/onboard.js
deleted file mode 100644
index 552bfa7..0000000
--- a/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/deploy/openclaw-railway/src/server.js b/deploy/openclaw-railway/src/server.js
deleted file mode 100644
index bb13b21..0000000
--- a/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/deploy/openclaw-railway/src/status.js b/deploy/openclaw-railway/src/status.js
deleted file mode 100644
index 1a920e9..0000000
--- a/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/deploy/openclaw-railway/workspace/AGENTS.md b/deploy/openclaw-railway/workspace/AGENTS.md
deleted file mode 100644
index cfff725..0000000
--- a/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/deploy/openclaw-railway/workspace/BOOTSTRAP.md b/deploy/openclaw-railway/workspace/BOOTSTRAP.md
deleted file mode 100644
index 524b44e..0000000
--- a/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/deploy/openclaw-railway/workspace/SOUL.md b/deploy/openclaw-railway/workspace/SOUL.md
deleted file mode 100644
index d79db94..0000000
--- a/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/deploy/openclaw-railway/workspace/TOOLS.md b/deploy/openclaw-railway/workspace/TOOLS.md
deleted file mode 100644
index 49964cb..0000000
--- a/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/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/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..97a45fb 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", "archive", ".*", "build", "dist"]
[tool.mypy]
python_version = "3.10"
@@ -55,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 99fe638..1538bca 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,42 +302,11 @@ 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":
- strategy = os.environ.get("STRATEGY", "engine_mm")
+ if mode == "strategy":
+ 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]
@@ -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)
@@ -390,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")
@@ -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")
@@ -594,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)
@@ -614,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/scripts/validate_agent_cli.py b/scripts/validate_agent_cli.py
index 5e25af4..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(
@@ -237,14 +234,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",
@@ -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/_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/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
new file mode 100644
index 0000000..d9b31b4
--- /dev/null
+++ b/tests/_archive/README.md
@@ -0,0 +1,11 @@
+# Archived strategy and quoting-engine tests
+
+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:
+
+```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_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_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_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..6206066 100644
--- a/tests/test_entrypoint.py
+++ b/tests/test_entrypoint.py
@@ -27,67 +27,33 @@ 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")
- 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_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_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",
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):
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..406d8a7 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):
@@ -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):
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):