diff --git a/README.md b/README.md
index 5d87330..27c0560 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
-
+
@@ -75,6 +75,21 @@ hl run engine_mm -i ETH-PERP --tick 10 --mainnet
hl apex run --mainnet
```
+### Funding Hedge
+
+Propose a read-only BTCSWP funding-rate hedge from the CLI or any MCP client. The default `hl hedge propose` path reads the current account position; passing `--perp-notional` switches to pure sizing mode with no account fetch or order execution.
+
+```bash
+hl hedge info --json
+hl hedge propose --asset BTC --side long --perp-notional 150000 --funding-apr 42
+hl hedge propose --asset BTC --side long --perp-notional 150000 --funding-rate-8h 0.0003 --json
+hl hedge backtest --csv funding.csv --asset BTC --side long --perp-notional 150000
+```
+
+Backtest CSVs need a `funding_rate_8h`, `perp_funding_rate_8h`, `funding_rate`, or `rate` column. Add `hedge_rate_8h`, `btcswp_rate_8h`, or `btcswp_funding_rate_8h` when you have realized BTCSWP rates; otherwise the backtest uses an idealized offset.
+
+MCP tools: `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`
+
---
## Strategies
@@ -119,7 +134,7 @@ Supporting strategies for portfolio management, block liquidity, and autonomous
| Strategy | Description | Key Parameters | When to Use |
|----------|-------------|----------------|-------------|
-| `hedge_agent` | Reduces excess exposure per deterministic mandate. Fires when net notional exceeds threshold. | `notional_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. |
+| `hedge_agent` | Inventory exposure reducer. Fires when net notional exceeds threshold. This is not the BTCSWP funding-rate hedge; use `hl hedge propose` / `hl hedge backtest` for that. | `notional_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. |
| `rfq_agent` | Block-size dark RFQ liquidity — quotes for large orders with wider spreads. | `min_size`, `spread_bps` | Institutional/block flow. Provides hidden liquidity for large counterparties. |
| `claude_agent` | Multi-model LLM trading agent. Sends market snapshot to an LLM (Gemini, Claude, or OpenAI), receives structured trade decisions. | `model`, `base_size` | Experimental/research. Autonomous decision-making using LLM reasoning. |
@@ -154,6 +169,7 @@ Built on the open [Agent Skills](https://agentskills.io) standard. Each skill is
| **[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) |
+| **[BTCSWP Funding Hedge](#btcswp-funding-hedge)** | Thin wrapper over the MCP-exposed BTCSWP funding hedge calculator for BTC perp hedge proposals and backtests. | [`SKILL.md`](skills/btcswp-funding-hedge/SKILL.md) |
### Install a skill (agents)
@@ -166,6 +182,7 @@ https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/radar/SKILL
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
+https://raw.githubusercontent.com/Nunchi-trade/agent-cli/main/skills/btcswp-funding-hedge/SKILL.md
```
### Install a skill (OpenClaw / ClawHub)
@@ -351,6 +368,20 @@ All adjustments have guardrail bounds — parameters can't swing wildly. Disable
---
+### BTCSWP Funding Hedge
+
+Agent skill wrapper for the existing read-only BTCSWP funding hedge calculator exposed over MCP. It sizes BTC perp funding hedges without duplicating the math or placing orders.
+
+```bash
+hl hedge propose --asset BTC --side long --perp-notional 150000 --funding-apr 42 --json
+```
+
+MCP tools: `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`
+
+**[Download SKILL.md](skills/btcswp-funding-hedge/SKILL.md)**
+
+---
+
### Production Safety
Built-in safety systems that protect positions even when the runner process crashes.
@@ -460,6 +491,9 @@ 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
+hl hedge info [--json] # Funding hedge profiles and schemas
+hl hedge propose [options] # BTCSWP funding hedge proposal
+hl hedge backtest --csv # Local funding hedge cashflow backtest
# Infrastructure
hl builder approve [--mainnet] # Approve builder fee
@@ -481,7 +515,7 @@ hl mcp serve # stdio transport (default)
hl mcp serve --transport sse # SSE transport
```
-**17 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`
+**20 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`
Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead.
@@ -618,7 +652,7 @@ hl run engine_mm -i BTCSWP-USDYP --tick 10
```
cli/ CLI commands and trading engine
commands/ Subcommand modules (run, apex, radar, pulse, guard, reflect, house, ...)
- mcp_server.py MCP server (16 tools via FastMCP)
+ 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)
diff --git a/cli/commands/hedge.py b/cli/commands/hedge.py
index b0e42f3..2292e59 100644
--- a/cli/commands/hedge.py
+++ b/cli/commands/hedge.py
@@ -150,15 +150,76 @@ def _build_proposal(hl, coin: str):
return proposal, snapshot
+# ─── info ────────────────────────────────────────────────────────────────────
+
+
+@hedge_app.command("info")
+def info_cmd(
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
+):
+ """Show deployed funding hedge capabilities and agent-facing schemas."""
+ from modules.funding_hedge import format_info, funding_hedge_info
+
+ info = funding_hedge_info()
+ if json_output:
+ typer.echo(json.dumps(info, indent=2))
+ else:
+ typer.echo(format_info(info))
+
+
# ─── propose ─────────────────────────────────────────────────────────────────
@hedge_app.command("propose")
def propose_cmd(
coin: str = typer.Argument("BTC", help="Coin to hedge (BTC, ETH)"),
+ asset: Optional[str] = typer.Option(None, "--asset", help="Alias for coin in pure sizing mode."),
mainnet: bool = typer.Option(False, "--mainnet", help="Use mainnet (default: testnet)"),
+ side: str = typer.Option("long", "--side", help="Perp exposure side for pure sizing: long or short"),
+ perp_notional: Optional[float] = typer.Option(
+ None,
+ "--perp-notional",
+ help="Pure sizing mode: absolute perp notional in USD; does not fetch account state.",
+ ),
+ funding_apr: Optional[float] = typer.Option(
+ None,
+ "--funding-apr",
+ help="Pure sizing mode: annualized funding APR. Accepts 0.42 or 42 for 42%.",
+ ),
+ funding_rate_8h: Optional[float] = typer.Option(
+ None,
+ "--funding-rate-8h",
+ help="Pure sizing mode: 8h funding rate as a decimal, e.g. 0.0003.",
+ ),
+ vol_multiplier: float = typer.Option(15.0, "--vol-multiplier", help="BTCSWP hedge multiplier."),
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON in pure sizing mode."),
):
- """Show a CFI v2 hedge proposal without executing."""
+ """Show a CFI v2 hedge proposal without executing.
+
+ By default this reads the current account position. Passing
+ `--perp-notional` switches to pure sizing mode for agents/docs/tests.
+ """
+ if perp_notional is not None:
+ from modules.funding_hedge import format_proposal, propose_funding_hedge
+
+ try:
+ proposal = propose_funding_hedge(
+ asset=asset or coin,
+ perp_side=side,
+ perp_notional_usd=perp_notional,
+ funding_apr=funding_apr,
+ funding_rate_8h=funding_rate_8h,
+ vol_multiplier=vol_multiplier,
+ )
+ except ValueError as exc:
+ raise typer.BadParameter(str(exc)) from exc
+
+ if json_output:
+ typer.echo(json.dumps(proposal.to_dict(), indent=2))
+ else:
+ typer.echo(format_proposal(proposal))
+ return
+
_boot_cli()
from cli.config import TradingConfig
@@ -379,8 +440,26 @@ def _refresh():
@hedge_app.command("backtest")
def backtest_cmd(
coin: str = typer.Option("BTC", "--coin", help="Coin (BTC or ETH)"),
+ asset: Optional[str] = typer.Option(None, "--asset", help="Alias for --coin in --csv mode."),
days: int = typer.Option(365, "--days", help="Backtest window"),
notional: float = typer.Option(1_000_000, "--notional", "-n"),
+ csv_path: Optional[Path] = typer.Option(
+ None,
+ "--csv",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ help="Pure local cashflow mode: CSV with funding_rate_8h/funding_rate and optional hedge_rate_8h.",
+ ),
+ side: str = typer.Option("long", "--side", help="Perp exposure side for --csv mode: long or short"),
+ perp_notional: Optional[float] = typer.Option(
+ None,
+ "--perp-notional",
+ help="Pure --csv mode: absolute perp notional in USD; overrides --notional.",
+ ),
+ vol_multiplier: float = typer.Option(15.0, "--vol-multiplier", help="BTCSWP hedge multiplier for --csv mode."),
+ json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON in --csv mode."),
script: Optional[Path] = typer.Option(
None,
"--script",
@@ -391,7 +470,28 @@ def backtest_cmd(
Shells out to `~/hyperliquid-funding-rate-perps/tools/hedge_calculator.py
--backtest --asset {COIN} --notional {N}`. Output is streamed through.
+ Passing `--csv` switches to pure local cashflow mode.
"""
+ if csv_path is not None:
+ from modules.funding_hedge import backtest_funding_hedge_csv, format_backtest
+
+ try:
+ backtest = backtest_funding_hedge_csv(
+ csv_path=csv_path,
+ asset=asset or coin,
+ perp_side=side,
+ perp_notional_usd=perp_notional if perp_notional is not None else notional,
+ vol_multiplier=vol_multiplier,
+ )
+ except ValueError as exc:
+ raise typer.BadParameter(str(exc)) from exc
+
+ if json_output:
+ typer.echo(json.dumps(backtest.to_dict(), indent=2))
+ else:
+ typer.echo(format_backtest(backtest))
+ return
+
_boot_cli()
script_path = script or (
diff --git a/cli/commands/setup.py b/cli/commands/setup.py
index 0e53dfa..4eb788b 100644
--- a/cli/commands/setup.py
+++ b/cli/commands/setup.py
@@ -19,6 +19,7 @@ def setup_check():
issues = []
ok_items = []
+ warnings = []
# 1. Python + hyperliquid SDK
try:
@@ -30,9 +31,16 @@ def setup_check():
# 2. Private key
has_env_key = bool(os.environ.get("HL_PRIVATE_KEY"))
from cli.keystore import list_keystores
+ from cli.web_auth import pairing_from_env
has_keystore = len(list_keystores()) > 0
+ pairing = pairing_from_env()
if has_env_key:
ok_items.append("HL_PRIVATE_KEY set")
+ if pairing is None:
+ warnings.append(
+ "Raw-key mode active. For MCP/agent use, prefer `hl pair connect` or hosted Nunchi Auth "
+ "so the AI client receives scoped access instead of a private key."
+ )
elif has_keystore:
ok_items.append(f"Keystore found ({len(list_keystores())} keys)")
from cli.keystore import _load_env_password
@@ -44,6 +52,13 @@ def setup_check():
issues.append("HL_KEYSTORE_PASSWORD not set (needed for auto-unlock)")
else:
issues.append("No private key: set HL_PRIVATE_KEY or run 'hl wallet import'")
+ if pairing is not None:
+ ok_items.append(f"web-auth pairing context provided ({pairing.address})")
+ else:
+ warnings.append(
+ "No web-auth pairing context found. Hosted/keyless signing uses "
+ "NUNCHI_WEB_AUTH_PAIR_TOKEN and NUNCHI_WEB_AUTH_ADDRESS."
+ )
# 3. Network
testnet = os.environ.get("HL_TESTNET", "true").lower()
@@ -86,6 +101,11 @@ def setup_check():
else:
typer.echo("\nAll checks passed.")
+ if warnings:
+ typer.echo("")
+ for warning in warnings:
+ typer.echo(f" WARN {warning}")
+
@setup_app.command("bootstrap")
def setup_bootstrap():
diff --git a/cli/mcp_server.py b/cli/mcp_server.py
index f59d31d..3f54491 100644
--- a/cli/mcp_server.py
+++ b/cli/mcp_server.py
@@ -29,7 +29,7 @@
"strategies", "builder_status", "wallet_list", "setup_check",
"account", "status", "apex_status",
"agent_memory", "trade_journal", "judge_report", "obsidian_context",
- "order_status", "funding_rates",
+ "order_status", "funding_rates", "funding_hedge_info", "funding_hedge_propose", "funding_hedge_backtest",
}
# Tools that move funds or cancel/close live orders/positions — handle with care.
_DESTRUCTIVE_TOOLS = {
@@ -356,7 +356,7 @@ def _ann(name: str, title: str):
"yex-trader",
instructions=(
"Autonomous Hyperliquid trading CLI — 14 strategies, APEX orchestrator, "
- "REFLECT reviews. Always confirm details with the user before calling "
+ "REFLECT reviews, BTCSWP funding hedge proposals. Always confirm details with the user before calling "
"destructive tools (trade, run_strategy, apex_run, schedule_cancel, "
"emergency_close_all). "
"emergency_close_all requires confirm=true."
@@ -472,6 +472,7 @@ def setup_check(ctx: FastMCPContext = None) -> str:
env_overrides = _request_env(ctx)
issues = []
ok_items = []
+ warnings = []
# SDK
try:
@@ -486,8 +487,14 @@ def setup_check(ctx: FastMCPContext = None) -> str:
env_overrides.get("NUNCHI_WEB_AUTH_ADDRESS")
)
keystores = list_keystores()
+ from cli.web_auth import pairing_from_env
+ pairing = pairing_from_env()
if has_env_key:
ok_items.append("HL_PRIVATE_KEY set")
+ if pairing is None and not has_web_auth:
+ warnings.append(
+ "Raw-key mode active. Prefer hl pair connect or hosted Nunchi Auth for MCP/agent use."
+ )
elif has_web_auth:
ok_items.append("web-auth pairing context provided")
elif keystores:
@@ -497,6 +504,13 @@ def setup_check(ctx: FastMCPContext = None) -> str:
"No signing context: set HL_PRIVATE_KEY, configure keystore, "
"or pass trusted web-auth pairing context"
)
+ if pairing is not None:
+ ok_items.append(f"web-auth pairing context provided ({pairing.address})")
+ elif not has_web_auth:
+ warnings.append(
+ "No web-auth pairing context found. Hosted/keyless signing uses "
+ "NUNCHI_WEB_AUTH_PAIR_TOKEN and NUNCHI_WEB_AUTH_ADDRESS."
+ )
# Network
testnet = os.environ.get("HL_TESTNET", "true").lower()
@@ -512,10 +526,87 @@ def setup_check(ctx: FastMCPContext = None) -> str:
return json.dumps({
"ok": ok_items,
+ "warnings": warnings,
"issues": issues,
"passed": len(issues) == 0,
}, indent=2)
+ @mcp.tool(**_ann("funding_hedge_info", "Funding hedge info"))
+ def funding_hedge_info() -> str:
+ """Describe deployed funding hedge profiles and input schemas."""
+ from modules.funding_hedge import funding_hedge_info as build_info
+
+ return json.dumps(build_info(), indent=2)
+
+ @mcp.tool(**_ann("funding_hedge_propose", "Funding hedge proposal"))
+ def funding_hedge_propose(
+ asset: str = "BTC",
+ perp_side: str = "long",
+ perp_notional_usd: float = 100_000.0,
+ funding_apr: Optional[float] = None,
+ funding_rate_8h: Optional[float] = None,
+ vol_multiplier: float = 15.0,
+ ) -> str:
+ """Propose a read-only BTCSWP funding-rate hedge.
+
+ Args:
+ asset: Underlying perp exposure. BTC is deployed today.
+ perp_side: Perp exposure side — "long" or "short".
+ perp_notional_usd: Absolute perp notional in USD.
+ funding_apr: Annualized funding APR. Accepts 0.42 or 42 for 42%.
+ funding_rate_8h: 8h funding rate as a decimal, used if funding_apr is omitted.
+ vol_multiplier: BTCSWP hedge multiplier. Default 15 means 1/15 notional.
+ """
+ from modules.funding_hedge import propose_funding_hedge
+
+ try:
+ proposal = propose_funding_hedge(
+ asset=asset,
+ perp_side=perp_side,
+ perp_notional_usd=perp_notional_usd,
+ funding_apr=funding_apr,
+ funding_rate_8h=funding_rate_8h,
+ vol_multiplier=vol_multiplier,
+ )
+ except ValueError as exc:
+ return json.dumps({"error": str(exc)}, indent=2)
+ return json.dumps(proposal.to_dict(), indent=2)
+
+ @mcp.tool(**_ann("funding_hedge_backtest", "Funding hedge backtest"))
+ def funding_hedge_backtest(
+ csv_path: str,
+ asset: str = "BTC",
+ perp_side: str = "long",
+ perp_notional_usd: float = 100_000.0,
+ vol_multiplier: float = 15.0,
+ ) -> str:
+ """Backtest BTCSWP funding hedge cashflows from a local CSV.
+
+ The CSV must include funding_rate_8h, perp_funding_rate_8h, funding_rate,
+ or rate. It may also include hedge_rate_8h, btcswp_rate_8h, or
+ btcswp_funding_rate_8h for realized hedge residuals.
+
+ Args:
+ csv_path: Local CSV path readable by the MCP server process.
+ asset: Underlying perp exposure. BTC is deployed today.
+ perp_side: Perp exposure side — "long" or "short".
+ perp_notional_usd: Absolute perp notional in USD.
+ vol_multiplier: BTCSWP hedge multiplier. Default 15 means 1/15 notional.
+ """
+ from modules.funding_hedge import backtest_funding_hedge_csv
+
+ try:
+ backtest = backtest_funding_hedge_csv(
+ csv_path=csv_path,
+ asset=asset,
+ perp_side=perp_side,
+ perp_notional_usd=perp_notional_usd,
+ vol_multiplier=vol_multiplier,
+ )
+ except (OSError, ValueError) as exc:
+ return json.dumps({"error": str(exc)}, indent=2)
+ return json.dumps(backtest.to_dict(), indent=2)
+
@mcp.tool(**_ann("account", "Account state"))
def account(mainnet: bool = False, ctx: FastMCPContext = None) -> str:
"""Get Hyperliquid account state (balances, positions)."""
diff --git a/cli/skill.md b/cli/skill.md
index 9cd0388..0b7b574 100644
--- a/cli/skill.md
+++ b/cli/skill.md
@@ -203,7 +203,7 @@ Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_chec
| 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 |
+| hedge_agent | Risk | Inventory exposure reducer; BTCSWP funding hedge lives under `hl hedge` |
| rfq_agent | RFQ | Block-size dark RFQ liquidity |
| claude_agent | LLM | Claude/Gemini-powered autonomous trading agent |
diff --git a/cli/strategy_registry.py b/cli/strategy_registry.py
index 61ea3eb..b7c29be 100644
--- a/cli/strategy_registry.py
+++ b/cli/strategy_registry.py
@@ -21,7 +21,7 @@
},
"hedge_agent": {
"path": "strategies.hedge_agent:HedgeAgent",
- "description": "inventory reducer (delta control)",
+ "description": "inventory reducer (delta control), not the BTCSWP funding-rate hedge",
"params": {"notional_threshold": 15000.0},
},
"cfi_hedge": {
diff --git a/modules/funding_hedge.py b/modules/funding_hedge.py
new file mode 100644
index 0000000..5cb0b31
--- /dev/null
+++ b/modules/funding_hedge.py
@@ -0,0 +1,421 @@
+"""Pure-math funding-rate hedge proposal helpers.
+
+This module intentionally does not talk to Hyperliquid or sign orders. It gives
+agents a deterministic way to size the public BTCSWP hedge slice.
+"""
+from __future__ import annotations
+
+import csv
+from dataclasses import asdict, dataclass
+from pathlib import Path
+from typing import Iterable, Literal, Optional
+
+
+Side = Literal["long", "short"]
+
+BTCSWP_PROFILE = {
+ "asset": "BTC",
+ "hedge_market": "BTCSWP-USDYP",
+ "hl_coin": "yex:BTCSWP",
+ "vol_multiplier": 15.0,
+ "status": "deployed",
+}
+
+ROADMAP_PROFILES = [
+ {"asset": "ETH", "hedge_market": "ETHSWP-USDYP", "status": "roadmap"},
+ {"asset": "HYPE", "hedge_market": "HYPESWP-USDYP", "status": "roadmap"},
+ {"asset": "SPCX", "hedge_market": "SPCXSWP-USDYP", "status": "roadmap"},
+]
+
+
+@dataclass(frozen=True)
+class FundingHedgeProposal:
+ asset: str
+ perp_side: Side
+ perp_notional_usd: float
+ funding_apr: float
+ funding_rate_8h: Optional[float]
+ hedge_market: str
+ hedge_hl_coin: str
+ hedge_side: Side
+ hedge_notional_usd: float
+ vol_multiplier: float
+ effective_hedged_notional_usd: float
+ coverage_pct: float
+ unhedged_funding_cashflow_usd_per_year: float
+ target_hedge_cashflow_usd_per_year: float
+ assumption: str
+ status: str
+ disclaimer: str
+
+ def to_dict(self) -> dict[str, object]:
+ return asdict(self)
+
+
+@dataclass(frozen=True)
+class FundingHedgeBacktestRow:
+ index: int
+ timestamp: Optional[str]
+ funding_rate_8h: float
+ hedge_rate_8h: float
+ unhedged_cashflow_usd: float
+ hedge_cashflow_usd: float
+ net_cashflow_usd: float
+
+ def to_dict(self) -> dict[str, object]:
+ return asdict(self)
+
+
+@dataclass(frozen=True)
+class FundingHedgeBacktest:
+ asset: str
+ perp_side: Side
+ perp_notional_usd: float
+ hedge_market: str
+ hedge_hl_coin: str
+ hedge_side: Side
+ hedge_notional_usd: float
+ vol_multiplier: float
+ effective_hedged_notional_usd: float
+ coverage_pct: float
+ periods: int
+ average_funding_rate_8h: float
+ annualized_average_funding_apr: float
+ unhedged_cashflow_usd: float
+ hedge_cashflow_usd: float
+ net_cashflow_usd: float
+ max_period_unhedged_payment_usd: float
+ max_period_net_cost_usd: float
+ rows: list[FundingHedgeBacktestRow]
+ assumption: str
+ disclaimer: str
+
+ def to_dict(self) -> dict[str, object]:
+ payload = asdict(self)
+ payload["rows"] = [row.to_dict() for row in self.rows]
+ return payload
+
+
+def funding_hedge_info() -> dict[str, object]:
+ """Return agent-discoverable metadata for the public hedge slice."""
+ return {
+ "name": "BTCSWP funding-rate hedge",
+ "summary": (
+ "Pure sizing and local cashflow backtesting for Nunchi's public BTC "
+ "funding-rate hedge surface."
+ ),
+ "deployed_profiles": [dict(BTCSWP_PROFILE)],
+ "roadmap_profiles": [dict(profile) for profile in ROADMAP_PROFILES],
+ "default_vol_multiplier": BTCSWP_PROFILE["vol_multiplier"],
+ "sizing_rule": "same-side BTCSWP, hedge_notional = perp_notional / vol_multiplier",
+ "supported_cli": [
+ "hl hedge propose --perp-notional ... --funding-apr ...",
+ "hl hedge backtest --csv ... --perp-notional ...",
+ ],
+ "mcp_tools": ["funding_hedge_info", "funding_hedge_propose", "funding_hedge_backtest"],
+ "csv_required_columns": ["funding_rate_8h", "perp_funding_rate_8h", "funding_rate", "rate"],
+ "csv_optional_columns": ["hedge_rate_8h", "btcswp_rate_8h", "btcswp_funding_rate_8h"],
+ "hedge_agent_distinction": (
+ "strategy hedge_agent is an inventory/delta reducer. The BTCSWP "
+ "funding-rate hedge lives under hl hedge and the funding_hedge_* MCP tools."
+ ),
+ "execution_boundary": (
+ "funding_hedge_info/propose/backtest do not place orders, sign payloads, "
+ "fetch private account state, or expose private rate methodology."
+ ),
+ }
+
+
+def normalize_side(side: str) -> Side:
+ normalized = side.strip().lower()
+ if normalized not in {"long", "short"}:
+ raise ValueError("side must be 'long' or 'short'")
+ return normalized # type: ignore[return-value]
+
+
+def normalize_apr(value: float) -> float:
+ """Accept either decimal APR (0.42) or percent APR (42)."""
+ if abs(value) > 1:
+ return value / 100.0
+ return value
+
+
+def annualize_funding_rate_8h(rate: float) -> float:
+ """Convert an 8h funding rate into simple annualized APR."""
+ return rate * 3 * 365
+
+
+def _normalize_rate(value: float) -> float:
+ """Accept decimals, or whole percent values when clearly percent-like."""
+ if abs(value) > 1:
+ return value / 100.0
+ return value
+
+
+def propose_funding_hedge(
+ *,
+ asset: str = "BTC",
+ perp_side: str = "long",
+ perp_notional_usd: float,
+ funding_apr: Optional[float] = None,
+ funding_rate_8h: Optional[float] = None,
+ vol_multiplier: float = BTCSWP_PROFILE["vol_multiplier"],
+) -> FundingHedgeProposal:
+ """Size a BTCSWP hedge for a BTC perp funding exposure.
+
+ Positive funding means longs pay shorts. The BTCSWP hedge is same-side and
+ sized at 1 / vol_multiplier notional so the rate leg targets the full perp
+ notional.
+ """
+ asset = asset.strip().upper()
+ if asset != "BTC":
+ raise ValueError("only BTC funding hedges are deployed today; ETH/HYPE/SPCX profiles are roadmap")
+ if perp_notional_usd <= 0:
+ raise ValueError("perp_notional_usd must be positive")
+ if vol_multiplier <= 0:
+ raise ValueError("vol_multiplier must be positive")
+ if funding_apr is None and funding_rate_8h is None:
+ raise ValueError("provide funding_apr or funding_rate_8h")
+
+ side = normalize_side(perp_side)
+ apr = annualize_funding_rate_8h(funding_rate_8h) if funding_apr is None else normalize_apr(funding_apr)
+ side_sign = 1 if side == "long" else -1
+
+ hedge_notional = perp_notional_usd / vol_multiplier
+ effective_notional = hedge_notional * vol_multiplier
+ unhedged_cashflow = -side_sign * perp_notional_usd * apr
+ target_hedge_cashflow = -unhedged_cashflow
+
+ return FundingHedgeProposal(
+ asset=asset,
+ perp_side=side,
+ perp_notional_usd=round(perp_notional_usd, 2),
+ funding_apr=apr,
+ funding_rate_8h=funding_rate_8h,
+ hedge_market=BTCSWP_PROFILE["hedge_market"],
+ hedge_hl_coin=BTCSWP_PROFILE["hl_coin"],
+ hedge_side=side,
+ hedge_notional_usd=round(hedge_notional, 2),
+ vol_multiplier=vol_multiplier,
+ effective_hedged_notional_usd=round(effective_notional, 2),
+ coverage_pct=round(effective_notional / perp_notional_usd * 100, 4),
+ unhedged_funding_cashflow_usd_per_year=round(unhedged_cashflow, 2),
+ target_hedge_cashflow_usd_per_year=round(target_hedge_cashflow, 2),
+ assumption=(
+ "BTCSWP hedge is same-side and uses 1/15 notional by default; "
+ "positive funding means long perps pay shorts."
+ ),
+ status=BTCSWP_PROFILE["status"],
+ disclaimer="Sizing proposal only. This command does not place orders or expose the private rate methodology.",
+ )
+
+
+def _first_present(row: dict[str, str], names: Iterable[str]) -> Optional[str]:
+ for name in names:
+ value = row.get(name)
+ if value not in (None, ""):
+ return value
+ return None
+
+
+def load_funding_rows_from_csv(path: str | Path) -> list[dict[str, Optional[str] | float]]:
+ """Load funding rows from CSV.
+
+ Required column aliases: funding_rate_8h, perp_funding_rate_8h, funding_rate, or rate.
+ Optional hedge aliases: hedge_rate_8h, btcswp_rate_8h, btcswp_funding_rate_8h.
+ """
+ csv_path = Path(path)
+ rows: list[dict[str, Optional[str] | float]] = []
+ with csv_path.open("r", encoding="utf-8", newline="") as handle:
+ reader = csv.DictReader(handle)
+ for index, raw in enumerate(reader, start=1):
+ normalized = {(key or "").strip().lower(): (value or "").strip() for key, value in raw.items()}
+ funding_raw = _first_present(
+ normalized,
+ ("funding_rate_8h", "perp_funding_rate_8h", "funding_rate", "rate"),
+ )
+ if funding_raw is None:
+ raise ValueError(
+ "CSV must include funding_rate_8h, perp_funding_rate_8h, funding_rate, or rate"
+ )
+ hedge_raw = _first_present(
+ normalized,
+ ("hedge_rate_8h", "btcswp_rate_8h", "btcswp_funding_rate_8h"),
+ )
+ try:
+ funding_rate = _normalize_rate(float(funding_raw))
+ hedge_rate = _normalize_rate(float(hedge_raw)) if hedge_raw is not None else funding_rate
+ except ValueError as exc:
+ raise ValueError(f"invalid funding rate on CSV row {index}") from exc
+ rows.append(
+ {
+ "timestamp": _first_present(normalized, ("timestamp", "time", "date")),
+ "funding_rate_8h": funding_rate,
+ "hedge_rate_8h": hedge_rate,
+ }
+ )
+ if not rows:
+ raise ValueError("CSV contains no funding rows")
+ return rows
+
+
+def backtest_funding_hedge(
+ *,
+ funding_rows: list[dict[str, Optional[str] | float]],
+ asset: str = "BTC",
+ perp_side: str = "long",
+ perp_notional_usd: float,
+ vol_multiplier: float = BTCSWP_PROFILE["vol_multiplier"],
+) -> FundingHedgeBacktest:
+ """Backtest funding cashflows for a same-side BTCSWP hedge."""
+ proposal = propose_funding_hedge(
+ asset=asset,
+ perp_side=perp_side,
+ perp_notional_usd=perp_notional_usd,
+ funding_rate_8h=float(funding_rows[0]["funding_rate_8h"]),
+ vol_multiplier=vol_multiplier,
+ )
+ side_sign = 1 if proposal.perp_side == "long" else -1
+
+ detail_rows: list[FundingHedgeBacktestRow] = []
+ for index, row in enumerate(funding_rows, start=1):
+ funding_rate = float(row["funding_rate_8h"])
+ hedge_rate = float(row["hedge_rate_8h"])
+ unhedged = -side_sign * perp_notional_usd * funding_rate
+ hedge = side_sign * proposal.effective_hedged_notional_usd * hedge_rate
+ net = unhedged + hedge
+ detail_rows.append(
+ FundingHedgeBacktestRow(
+ index=index,
+ timestamp=str(row["timestamp"]) if row.get("timestamp") else None,
+ funding_rate_8h=funding_rate,
+ hedge_rate_8h=hedge_rate,
+ unhedged_cashflow_usd=round(unhedged, 2),
+ hedge_cashflow_usd=round(hedge, 2),
+ net_cashflow_usd=round(net, 2),
+ )
+ )
+
+ periods = len(detail_rows)
+ avg_rate = sum(row.funding_rate_8h for row in detail_rows) / periods
+ unhedged_total = sum(row.unhedged_cashflow_usd for row in detail_rows)
+ hedge_total = sum(row.hedge_cashflow_usd for row in detail_rows)
+ net_total = sum(row.net_cashflow_usd for row in detail_rows)
+ max_unhedged_payment = max(max(-row.unhedged_cashflow_usd, 0.0) for row in detail_rows)
+ max_net_cost = max(max(-row.net_cashflow_usd, 0.0) for row in detail_rows)
+
+ return FundingHedgeBacktest(
+ asset=proposal.asset,
+ perp_side=proposal.perp_side,
+ perp_notional_usd=proposal.perp_notional_usd,
+ hedge_market=proposal.hedge_market,
+ hedge_hl_coin=proposal.hedge_hl_coin,
+ hedge_side=proposal.hedge_side,
+ hedge_notional_usd=proposal.hedge_notional_usd,
+ vol_multiplier=proposal.vol_multiplier,
+ effective_hedged_notional_usd=proposal.effective_hedged_notional_usd,
+ coverage_pct=proposal.coverage_pct,
+ periods=periods,
+ average_funding_rate_8h=round(avg_rate, 10),
+ annualized_average_funding_apr=round(annualize_funding_rate_8h(avg_rate), 6),
+ unhedged_cashflow_usd=round(unhedged_total, 2),
+ hedge_cashflow_usd=round(hedge_total, 2),
+ net_cashflow_usd=round(net_total, 2),
+ max_period_unhedged_payment_usd=round(max_unhedged_payment, 2),
+ max_period_net_cost_usd=round(max_net_cost, 2),
+ rows=detail_rows,
+ assumption=(
+ "If no hedge_rate_8h/BTCSWP column is supplied, the backtest assumes "
+ "the BTCSWP hedge rate equals the perp funding rate for an idealized offset."
+ ),
+ disclaimer="Backtest is local cashflow math only. It does not place orders or model liquidity, fees, or mark-to-market.",
+ )
+
+
+def backtest_funding_hedge_csv(
+ *,
+ csv_path: str | Path,
+ asset: str = "BTC",
+ perp_side: str = "long",
+ perp_notional_usd: float,
+ vol_multiplier: float = BTCSWP_PROFILE["vol_multiplier"],
+) -> FundingHedgeBacktest:
+ return backtest_funding_hedge(
+ funding_rows=load_funding_rows_from_csv(csv_path),
+ asset=asset,
+ perp_side=perp_side,
+ perp_notional_usd=perp_notional_usd,
+ vol_multiplier=vol_multiplier,
+ )
+
+
+def format_proposal(proposal: FundingHedgeProposal) -> str:
+ direction = "paying" if proposal.unhedged_funding_cashflow_usd_per_year < 0 else "receiving"
+ return "\n".join(
+ [
+ "Funding Hedge Proposal",
+ "=" * 40,
+ f"Exposure: {proposal.perp_side.upper()} {proposal.asset} perp ${proposal.perp_notional_usd:,.2f}",
+ f"Funding APR: {proposal.funding_apr * 100:,.2f}%",
+ f"Unhedged leg: {direction} ${abs(proposal.unhedged_funding_cashflow_usd_per_year):,.2f}/yr",
+ "",
+ f"Hedge market: {proposal.hedge_market} ({proposal.hedge_hl_coin})",
+ f"Hedge action: {proposal.hedge_side.upper()} ${proposal.hedge_notional_usd:,.2f}",
+ f"Multiplier: {proposal.vol_multiplier:,.2f}x",
+ f"Coverage: ${proposal.effective_hedged_notional_usd:,.2f} ({proposal.coverage_pct:.2f}%)",
+ f"Target offset: ${proposal.target_hedge_cashflow_usd_per_year:,.2f}/yr",
+ "",
+ f"Assumption: {proposal.assumption}",
+ f"Status: {proposal.status}",
+ f"Disclaimer: {proposal.disclaimer}",
+ ]
+ )
+
+
+def format_info(info: dict[str, object]) -> str:
+ profiles = info.get("deployed_profiles", [])
+ deployed = profiles[0] if isinstance(profiles, list) and profiles else {}
+ if not isinstance(deployed, dict):
+ deployed = {}
+ return "\n".join(
+ [
+ "Funding Hedge Info",
+ "=" * 40,
+ f"Name: {info['name']}",
+ f"Summary: {info['summary']}",
+ f"Deployed: {deployed.get('asset', 'BTC')} -> {deployed.get('hedge_market', 'BTCSWP-USDYP')}",
+ f"Multiplier: {info['default_vol_multiplier']}x",
+ f"Sizing rule: {info['sizing_rule']}",
+ "",
+ "CLI:",
+ *[f" {cmd}" for cmd in info["supported_cli"]], # type: ignore[index]
+ "",
+ "MCP:",
+ *[f" {tool}" for tool in info["mcp_tools"]], # type: ignore[index]
+ "",
+ f"CSV required: {', '.join(info['csv_required_columns'])}", # type: ignore[arg-type]
+ f"CSV optional: {', '.join(info['csv_optional_columns'])}", # type: ignore[arg-type]
+ f"Note: {info['hedge_agent_distinction']}",
+ ]
+ )
+
+
+def format_backtest(backtest: FundingHedgeBacktest) -> str:
+ return "\n".join(
+ [
+ "Funding Hedge Backtest",
+ "=" * 40,
+ f"Exposure: {backtest.perp_side.upper()} {backtest.asset} perp ${backtest.perp_notional_usd:,.2f}",
+ f"Hedge: {backtest.hedge_side.upper()} ${backtest.hedge_notional_usd:,.2f} {backtest.hedge_market}",
+ f"Periods: {backtest.periods}",
+ f"Avg funding APR: {backtest.annualized_average_funding_apr * 100:,.2f}%",
+ "",
+ f"Unhedged cashflow:{backtest.unhedged_cashflow_usd:>15,.2f} USD",
+ f"Hedge cashflow: {backtest.hedge_cashflow_usd:>15,.2f} USD",
+ f"Net cashflow: {backtest.net_cashflow_usd:>15,.2f} USD",
+ f"Max net cost: {backtest.max_period_net_cost_usd:>15,.2f} USD / period",
+ "",
+ f"Assumption: {backtest.assumption}",
+ f"Disclaimer: {backtest.disclaimer}",
+ ]
+ )
diff --git a/skills/btcswp-funding-hedge/SKILL.md b/skills/btcswp-funding-hedge/SKILL.md
new file mode 100644
index 0000000..7b699f5
--- /dev/null
+++ b/skills/btcswp-funding-hedge/SKILL.md
@@ -0,0 +1,135 @@
+---
+name: btcswp-funding-hedge
+version: 1.0.0
+description: Calculate Nunchi BTCSWP funding hedge proposals through the existing MCP-exposed funding hedge calculator. Use when sizing BTC perp funding hedges, BTCSWP hedges, or Nunchi hedge proposals.
+author: Nunchi Trade
+tags: [btcswp, funding, hedge, btc, mcp, nunchi]
+compatibility: Requires the yex-trader agent-cli MCP server with funding_hedge_info, funding_hedge_propose, and funding_hedge_backtest available.
+metadata:
+ platform: yex-trader
+ exchange: hyperliquid
+ category: funding-hedge
+---
+
+# BTCSWP Funding Hedge
+
+Package the Nunchi BTCSWP funding hedge calculator for agents. This skill is a thin wrapper over the existing MCP tools:
+
+- `funding_hedge_info`
+- `funding_hedge_propose`
+- `funding_hedge_backtest`
+
+Do not reimplement, approximate, or edit the hedge math. The calculator lives in `modules/funding_hedge.py` and the MCP tools expose its output.
+
+## Agent Mandate
+
+You are sizing a read-only BTCSWP funding hedge for a BTC perp exposure. Your job is to collect the required inputs, call the Nunchi MCP tool, validate that it returned a proposal instead of an error, and present the returned hedge fields clearly.
+
+RULES:
+- Use `funding_hedge_info` when you need to discover deployed assets, supported inputs, or caveats.
+- ALWAYS call `funding_hedge_propose` for a new hedge proposal.
+- NEVER calculate hedge notional yourself.
+- NEVER place orders from this skill. It returns sizing only.
+- NEVER claim support for ETH, HYPE, SPCX, or other assets unless the MCP tool accepts them.
+- ALWAYS provide either `funding_apr` or `funding_rate_8h`.
+- Treat `hedge_hl_coin` as the Hyperliquid coin identifier to use in downstream execution flows.
+
+## Proposal Inputs
+
+Call `funding_hedge_propose` with:
+
+```json
+{
+ "asset": "BTC",
+ "perp_side": "long",
+ "perp_notional_usd": 150000,
+ "funding_apr": 42,
+ "vol_multiplier": 15.0
+}
+```
+
+Input rules:
+- `asset`: `BTC` is deployed today.
+- `perp_side`: `long` or `short`.
+- `perp_notional_usd`: absolute BTC perp notional in USD.
+- `funding_apr`: annualized funding APR. The tool accepts `0.42` or `42` for 42%.
+- `funding_rate_8h`: optional alternative to `funding_apr`, as a decimal like `0.0003`.
+- `vol_multiplier`: optional. Default is `15.0`.
+
+## Proposal Output
+
+The MCP tool returns JSON. If it returns `{"error": "..."}`, stop and report the error.
+
+Key fields to present:
+- `asset`
+- `perp_side`
+- `perp_notional_usd`
+- `funding_apr`
+- `hedge_market`
+- `hedge_hl_coin`
+- `hedge_side`
+- `hedge_notional_usd`
+- `vol_multiplier`
+- `effective_hedged_notional_usd`
+- `coverage_pct`
+- `unhedged_funding_cashflow_usd_per_year`
+- `target_hedge_cashflow_usd_per_year`
+- `assumption`
+- `disclaimer`
+
+There is no separate hedge-card schema in this repo. If the user asks for a hedge card, format the returned MCP fields as a concise summary card without changing values:
+
+```markdown
+BTCSWP Hedge
+Exposure: [perp_side] BTC perp $[perp_notional_usd]
+Hedge: [hedge_side] $[hedge_notional_usd] [hedge_market] ([hedge_hl_coin])
+Coverage: [coverage_pct]% via [vol_multiplier]x multiplier
+Funding APR: [funding_apr as percent]
+Unhedged funding: $[abs(unhedged_funding_cashflow_usd_per_year)]/yr [paying or receiving]
+Target offset: $[target_hedge_cashflow_usd_per_year]/yr
+Status: sizing only; no order placed
+```
+
+## Backtest
+
+Use `funding_hedge_backtest` only when the user has a local CSV path visible to the MCP server process.
+
+Call shape:
+
+```json
+{
+ "csv_path": "/path/to/funding.csv",
+ "asset": "BTC",
+ "perp_side": "long",
+ "perp_notional_usd": 150000,
+ "vol_multiplier": 15.0
+}
+```
+
+CSV requirements:
+- Required funding column: `funding_rate_8h`, `perp_funding_rate_8h`, `funding_rate`, or `rate`.
+- Optional BTCSWP realized hedge column: `hedge_rate_8h`, `btcswp_rate_8h`, or `btcswp_funding_rate_8h`.
+- Optional timestamp column: `timestamp`, `time`, or `date`.
+
+## CLI Fallback
+
+Use the MCP tools when the user has a connected agent. If MCP is unavailable and the repo is installed locally, pure sizing mode is:
+
+```bash
+hl hedge propose --asset BTC --side long --perp-notional 150000 --funding-apr 42 --json
+```
+
+Passing `--perp-notional` keeps this in pure sizing mode. Without `--perp-notional`, the CLI may read account state and belongs to a separate connected-wallet flow.
+
+## Verification Example
+
+For `BTC`, `long`, `$150,000` perp notional, and `42%` APR, the MCP proposal should return:
+
+- `hedge_market`: `BTCSWP-USDYP`
+- `hedge_hl_coin`: `yex:BTCSWP`
+- `hedge_side`: `long`
+- `hedge_notional_usd`: `10000.0`
+- `coverage_pct`: `100.0`
+- `unhedged_funding_cashflow_usd_per_year`: `-63000.0`
+- `target_hedge_cashflow_usd_per_year`: `63000.0`
+
diff --git a/strategies/hedge_agent.py b/strategies/hedge_agent.py
index 5068ad4..2d13d36 100644
--- a/strategies/hedge_agent.py
+++ b/strategies/hedge_agent.py
@@ -1,8 +1,11 @@
-"""Hedge agent — reduces excess exposure per deterministic mandate.
+"""Hedge agent — reduces inventory exposure per deterministic mandate.
From KorAI spec: "reduces exposure per deterministic mandate."
Only acts when |inventory| exceeds a configurable threshold,
then places aggressive orders to bring inventory back toward zero.
+
+This is not the BTCSWP funding-rate hedge. Use `hl hedge propose` or
+`hl hedge backtest` for the public funding hedge tooling.
"""
from __future__ import annotations
@@ -13,7 +16,7 @@
class HedgeAgent(BaseStrategy):
- """Deterministic hedge agent that reduces inventory when overexposed."""
+ """Deterministic inventory hedge agent that reduces overexposure."""
def __init__(
self,
diff --git a/tests/test_funding_hedge.py b/tests/test_funding_hedge.py
new file mode 100644
index 0000000..aae315d
--- /dev/null
+++ b/tests/test_funding_hedge.py
@@ -0,0 +1,233 @@
+"""Tests for BTCSWP funding hedge proposal surfaces."""
+from __future__ import annotations
+
+import json
+import sys
+import types
+
+from typer.testing import CliRunner
+
+from cli.main import app
+from modules.funding_hedge import (
+ annualize_funding_rate_8h,
+ backtest_funding_hedge_csv,
+ funding_hedge_info,
+ propose_funding_hedge,
+)
+
+
+runner = CliRunner()
+
+
+class FakeFastMCP:
+ def __init__(self, *args, **kwargs):
+ self.tools = {}
+
+ def tool(self, *args, **kwargs):
+ def decorator(fn):
+ self.tools[fn.__name__] = fn
+ return fn
+
+ return decorator
+
+
+def install_fake_mcp(monkeypatch) -> None:
+ fastmcp_module = types.ModuleType("mcp.server.fastmcp")
+ fastmcp_module.FastMCP = FakeFastMCP
+ server_module = types.ModuleType("mcp.server")
+ server_module.fastmcp = fastmcp_module
+ mcp_module = types.ModuleType("mcp")
+ mcp_module.server = server_module
+ monkeypatch.setitem(sys.modules, "mcp", mcp_module)
+ monkeypatch.setitem(sys.modules, "mcp.server", server_module)
+ monkeypatch.setitem(sys.modules, "mcp.server.fastmcp", fastmcp_module)
+
+
+def test_propose_btcswp_funding_hedge_percent_apr():
+ proposal = propose_funding_hedge(
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000,
+ funding_apr=42,
+ )
+
+ assert proposal.hedge_market == "BTCSWP-USDYP"
+ assert proposal.hedge_side == "long"
+ assert proposal.hedge_notional_usd == 10_000
+ assert proposal.effective_hedged_notional_usd == 150_000
+ assert proposal.funding_apr == 0.42
+ assert proposal.unhedged_funding_cashflow_usd_per_year == -63_000
+ assert proposal.target_hedge_cashflow_usd_per_year == 63_000
+
+
+def test_propose_annualizes_8h_funding_rate():
+ apr = annualize_funding_rate_8h(0.0003)
+ proposal = propose_funding_hedge(
+ asset="BTC",
+ perp_side="short",
+ perp_notional_usd=90_000,
+ funding_rate_8h=0.0003,
+ )
+
+ assert proposal.funding_apr == apr
+ assert proposal.hedge_notional_usd == 6_000
+ assert proposal.unhedged_funding_cashflow_usd_per_year == 29_565
+
+
+def test_funding_hedge_info_describes_deployed_profile():
+ info = funding_hedge_info()
+
+ assert info["deployed_profiles"][0]["asset"] == "BTC" # type: ignore[index]
+ assert info["deployed_profiles"][0]["hedge_market"] == "BTCSWP-USDYP" # type: ignore[index]
+ assert "funding_hedge_info" in info["mcp_tools"]
+ assert "funding_rate_8h" in info["csv_required_columns"]
+ assert "hedge_agent is an inventory/delta reducer" in info["hedge_agent_distinction"]
+
+
+def test_hedge_propose_cli_json():
+ result = runner.invoke(
+ app,
+ ["hedge", "propose", "--perp-notional", "150000", "--side", "long", "--funding-apr", "42", "--json"],
+ )
+
+ assert result.exit_code == 0
+ payload = json.loads(result.stdout)
+ assert payload["hedge_market"] == "BTCSWP-USDYP"
+ assert payload["hedge_notional_usd"] == 10_000
+ assert payload["disclaimer"].startswith("Sizing proposal only.")
+
+
+def test_hedge_info_cli_json():
+ result = runner.invoke(app, ["hedge", "info", "--json"])
+
+ assert result.exit_code == 0
+ payload = json.loads(result.stdout)
+ assert payload["deployed_profiles"][0]["hedge_market"] == "BTCSWP-USDYP"
+ assert "funding_hedge_backtest" in payload["mcp_tools"]
+
+
+def test_mcp_funding_hedge_propose(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(
+ server.tools["funding_hedge_propose"](
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000,
+ funding_apr=42,
+ )
+ )
+
+ assert payload["hedge_market"] == "BTCSWP-USDYP"
+ assert payload["hedge_side"] == "long"
+ assert payload["hedge_notional_usd"] == 10_000
+ assert payload["coverage_pct"] == 100
+
+
+def test_mcp_funding_hedge_info(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["funding_hedge_info"]())
+
+ assert payload["deployed_profiles"][0]["asset"] == "BTC"
+ assert "funding_hedge_propose" in payload["mcp_tools"]
+
+
+def test_mcp_funding_hedge_rejects_roadmap_assets(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["funding_hedge_propose"](asset="ETH", funding_apr=10))
+
+ assert "only BTC funding hedges are deployed today" in payload["error"]
+
+
+def test_backtest_csv_idealized_offset(tmp_path):
+ csv_path = tmp_path / "funding.csv"
+ csv_path.write_text("timestamp,funding_rate_8h\n1,0.0003\n2,-0.0001\n", "utf-8")
+
+ backtest = backtest_funding_hedge_csv(
+ csv_path=csv_path,
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000,
+ )
+
+ assert backtest.periods == 2
+ assert backtest.hedge_notional_usd == 10_000
+ assert backtest.unhedged_cashflow_usd == -30
+ assert backtest.hedge_cashflow_usd == 30
+ assert backtest.net_cashflow_usd == 0
+
+
+def test_backtest_csv_realized_hedge_residual(tmp_path):
+ csv_path = tmp_path / "funding.csv"
+ csv_path.write_text("date,funding_rate_8h,btcswp_rate_8h\n2026-01-01,0.0003,0.00025\n", "utf-8")
+
+ backtest = backtest_funding_hedge_csv(
+ csv_path=csv_path,
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000,
+ )
+
+ assert backtest.unhedged_cashflow_usd == -45
+ assert backtest.hedge_cashflow_usd == 37.5
+ assert backtest.net_cashflow_usd == -7.5
+ assert backtest.max_period_net_cost_usd == 7.5
+
+
+def test_hedge_backtest_cli_json(tmp_path):
+ csv_path = tmp_path / "funding.csv"
+ csv_path.write_text("funding_rate\n0.0003\n-0.0001\n", "utf-8")
+
+ result = runner.invoke(
+ app,
+ [
+ "hedge",
+ "backtest",
+ "--csv",
+ str(csv_path),
+ "--perp-notional",
+ "150000",
+ "--side",
+ "long",
+ "--json",
+ ],
+ )
+
+ assert result.exit_code == 0
+ payload = json.loads(result.stdout)
+ assert payload["periods"] == 2
+ assert payload["hedge_market"] == "BTCSWP-USDYP"
+ assert payload["net_cashflow_usd"] == 0
+
+
+def test_mcp_funding_hedge_backtest(monkeypatch, tmp_path):
+ csv_path = tmp_path / "funding.csv"
+ csv_path.write_text("funding_rate_8h\n0.0003\n", "utf-8")
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(
+ server.tools["funding_hedge_backtest"](
+ csv_path=str(csv_path),
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000,
+ )
+ )
+
+ assert payload["periods"] == 1
+ assert payload["unhedged_cashflow_usd"] == -45
+ assert payload["hedge_cashflow_usd"] == 45
diff --git a/tests/test_setup_auth_guidance.py b/tests/test_setup_auth_guidance.py
new file mode 100644
index 0000000..9ad8bd9
--- /dev/null
+++ b/tests/test_setup_auth_guidance.py
@@ -0,0 +1,70 @@
+"""Tests for setup auth-mode guidance."""
+from __future__ import annotations
+
+import json
+import sys
+import types
+
+from typer.testing import CliRunner
+
+from cli.commands.setup import setup_app
+
+
+runner = CliRunner()
+
+
+class FakeFastMCP:
+ def __init__(self, *args, **kwargs):
+ self.tools = {}
+
+ def tool(self, *args, **kwargs):
+ def decorator(fn):
+ self.tools[fn.__name__] = fn
+ return fn
+
+ return decorator
+
+
+def install_fake_mcp(monkeypatch) -> None:
+ fastmcp_module = types.ModuleType("mcp.server.fastmcp")
+ fastmcp_module.FastMCP = FakeFastMCP
+ server_module = types.ModuleType("mcp.server")
+ server_module.fastmcp = fastmcp_module
+ mcp_module = types.ModuleType("mcp")
+ mcp_module.server = server_module
+ monkeypatch.setitem(sys.modules, "mcp", mcp_module)
+ monkeypatch.setitem(sys.modules, "mcp.server", server_module)
+ monkeypatch.setitem(sys.modules, "mcp.server.fastmcp", fastmcp_module)
+
+
+def install_setup_fakes(monkeypatch, paired_wallet=None) -> None:
+ monkeypatch.setitem(sys.modules, "hyperliquid", types.ModuleType("hyperliquid"))
+ monkeypatch.setattr("cli.keystore.list_keystores", lambda: [])
+ monkeypatch.setattr("cli.web_auth.pairing_from_env", lambda: paired_wallet)
+
+
+def test_setup_check_warns_on_raw_key_without_pairing(monkeypatch):
+ install_setup_fakes(monkeypatch)
+ monkeypatch.setenv("HL_PRIVATE_KEY", "0x" + "1" * 64)
+
+ result = runner.invoke(setup_app, ["check"])
+
+ assert result.exit_code == 0
+ assert "HL_PRIVATE_KEY set" in result.output
+ assert "Raw-key mode active" in result.output
+ assert "NUNCHI_WEB_AUTH_PAIR_TOKEN" in result.output
+
+
+def test_mcp_setup_check_reports_auth_warnings(monkeypatch):
+ install_setup_fakes(monkeypatch)
+ install_fake_mcp(monkeypatch)
+ monkeypatch.setenv("HL_PRIVATE_KEY", "0x" + "1" * 64)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["setup_check"]())
+
+ assert "HL_PRIVATE_KEY set" in payload["ok"]
+ assert any("Raw-key mode active" in warning for warning in payload["warnings"])
+ assert any("No web-auth pairing context found" in warning for warning in payload["warnings"])