Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<img src="https://img.shields.io/badge/strategies-14-C9A84C" alt="Strategies" />
<img src="https://img.shields.io/badge/tests-483%20passing-brightgreen" alt="Tests" />
<img src="https://img.shields.io/badge/license-MIT-blue" alt="License" />
<img src="https://img.shields.io/badge/MCP-24%20tools-8A2BE2" alt="MCP" />
<img src="https://img.shields.io/badge/MCP-25%20tools-8A2BE2" alt="MCP" />
</p>

<p align="center">
Expand Down Expand Up @@ -77,18 +77,21 @@ 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.
Propose, backtest, or execute a 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 auth import --token <scoped-token> --address 0x... --permission-tier testnet_trading
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 execute BTC --dry-run
hl hedge execute BTC --yes
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`
MCP tools: `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`. Live MCP execution requires `confirmed=true` plus a signing context from `HL_PRIVATE_KEY`, keystore, trusted hosted context, or a local scoped token stored with `hl auth import`.

---

Expand Down Expand Up @@ -477,9 +480,11 @@ 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 execute BTC [--dry-run] # Execute or preview BTCSWP hedge
hl hedge backtest --csv <path> # Local funding hedge cashflow backtest

# Infrastructure
hl auth import/status/export-env # Local scoped-token keyless auth
hl builder approve [--mainnet] # Approve builder fee
hl wallet auto [--save-env] # Create wallet (agent-friendly)
hl setup check # Validate environment
Expand Down Expand Up @@ -531,7 +536,7 @@ hl mcp serve # stdio transport (default)
hl mcp serve --transport sse # SSE transport
```

**24 tools exposed:** `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`
**25 tools exposed:** `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `funding_hedge_info`, `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`

Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. Local MCP is for development and agent harness testing only; hosted deployment goes through Nunchi Auth and the subscription-gated hosted-agent flow.

Expand Down Expand Up @@ -594,7 +599,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 (24 tools via FastMCP)
mcp_server.py MCP server (25 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)
Expand Down
133 changes: 133 additions & 0 deletions cli/commands/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""hl auth — local scoped-token management for keyless agent flows."""
from __future__ import annotations

import json
import time
from typing import Optional

import typer

auth_app = typer.Typer(no_args_is_help=True)


def _redact(token: str) -> str:
if len(token) <= 12:
return token[:2] + "..."
return token[:6] + "..." + token[-4:]


@auth_app.command("import", help="Store a scoped Nunchi web-auth token locally")
def auth_import(
token: str = typer.Option(..., "--token", prompt=True, hide_input=True, help="Scoped web-auth token."),
address: str = typer.Option(..., "--address", help="Authorized wallet address."),
account_id: str = typer.Option("", "--account-id", help="Optional Nunchi account id."),
permission_tier: str = typer.Option(
"testnet_trading",
"--permission-tier",
help="read_only, testnet_trading, or live_trading.",
),
network: str = typer.Option("testnet", "--network", help="testnet or mainnet."),
allow_mainnet: bool = typer.Option(False, "--allow-mainnet", help="Allow mainnet actions."),
max_order_size: Optional[float] = typer.Option(None, "--max-order-size", help="Optional max order size."),
max_hedge_notional: Optional[float] = typer.Option(
None,
"--max-hedge-notional",
help="Optional max BTCSWP hedge notional in USD.",
),
max_strategy_ticks: Optional[int] = typer.Option(None, "--max-strategy-ticks", help="Optional max ticks."),
require_confirmation: bool = typer.Option(
True,
"--require-confirmation/--no-require-confirmation",
help="Require confirmed=true for hosted/MCP write tools.",
),
json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
) -> None:
"""Persist a scoped token so local CLI/MCP can sign without raw private keys."""
from cli.web_auth import ScopedToken, save_scoped_token

tier = permission_tier.strip().lower()
if tier not in {"read_only", "testnet_trading", "live_trading"}:
raise typer.BadParameter("permission-tier must be read_only, testnet_trading, or live_trading")
net = network.strip().lower()
if net not in {"testnet", "mainnet"}:
raise typer.BadParameter("network must be testnet or mainnet")

scoped = ScopedToken(
token=token.strip(),
address=address.strip(),
account_id=account_id.strip(),
permission_tier=tier,
network=net,
allow_mainnet=allow_mainnet,
max_order_size=max_order_size,
max_hedge_notional=max_hedge_notional,
max_strategy_ticks=max_strategy_ticks,
require_confirmation=require_confirmation,
created_at_ms=int(time.time() * 1000),
)
path = save_scoped_token(scoped)
payload = {
"stored": True,
"path": str(path),
"address": scoped.address,
"permission_tier": scoped.permission_tier,
"network": scoped.network,
"allow_mainnet": scoped.allow_mainnet,
"token": _redact(scoped.token),
}
typer.echo(json.dumps(payload, indent=2) if json_output else f"Stored scoped token for {scoped.address} at {path}")


@auth_app.command("status", help="Show stored scoped-token status")
def auth_status(json_output: bool = typer.Option(False, "--json", help="Output machine-readable JSON.")) -> None:
from cli.web_auth import load_scoped_token, scoped_token_path

scoped = load_scoped_token()
if scoped is None:
payload = {"configured": False, "path": str(scoped_token_path())}
else:
payload = {
"configured": True,
"path": str(scoped_token_path()),
"address": scoped.address,
"account_id": scoped.account_id,
"permission_tier": scoped.permission_tier,
"network": scoped.network,
"allow_mainnet": scoped.allow_mainnet,
"max_order_size": scoped.max_order_size,
"max_hedge_notional": scoped.max_hedge_notional,
"max_strategy_ticks": scoped.max_strategy_ticks,
"require_confirmation": scoped.require_confirmation,
"token": _redact(scoped.token),
}
if json_output:
typer.echo(json.dumps(payload, indent=2))
elif not payload["configured"]:
typer.echo(f"No scoped token configured at {payload['path']}")
else:
typer.echo(
f"Scoped token active for {payload['address']} "
f"({payload['permission_tier']}, {payload['network']})"
)


@auth_app.command("export-env", help="Print shell exports for the stored scoped token")
def auth_export_env() -> None:
from cli.web_auth import scoped_token_env

env = scoped_token_env()
if not env:
typer.echo("No scoped token configured.", err=True)
raise typer.Exit(1)
for key, value in env.items():
escaped = value.replace("'", "'\"'\"'")
typer.echo(f"export {key}='{escaped}'")


@auth_app.command("revoke", help="Delete the local scoped token")
def auth_revoke() -> None:
from cli.web_auth import clear_scoped_token, scoped_token_path

path = scoped_token_path()
clear_scoped_token()
typer.echo(f"Removed local scoped token at {path}")
43 changes: 42 additions & 1 deletion cli/commands/hedge.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ 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)"),
max_hedge_notional: Optional[float] = typer.Option(
None,
"--max-hedge-notional",
help="Reject execution if proposed BTCSWP hedge notional exceeds this USD cap.",
),
policy: Optional[Path] = typer.Option(
None,
"--policy",
help="Session policy file (or inline JSON / NUNCHI_SESSION_POLICY env).",
),
):
"""Build the proposal and optionally sign + submit a real yex:{COIN}SWP order.

Expand All @@ -256,8 +266,13 @@ 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.session_policy import ACTION_HEDGE, current_workspace, guard_or_exit
from parent.hl_proxy import HLProxy

network = "mainnet" if mainnet else "testnet"
policy_path = str(policy) if policy else None
guard_or_exit(ACTION_HEDGE, policy_path=policy_path, network=network)

cfg = TradingConfig()
private_key = cfg.get_private_key()
raw_hl = HLProxy(private_key=private_key, testnet=not mainnet)
Expand All @@ -266,6 +281,23 @@ def execute_cmd(
proposal, snapshot = _build_proposal(hl, coin)
typer.echo(hedge_proposal_block(proposal, snapshot, mainnet=mainnet))

if max_hedge_notional is not None and proposal.hedge_notional_usd > max_hedge_notional:
Comment thread
JaeLeex marked this conversation as resolved.
typer.echo(
f"Refusing hedge: proposed notional ${proposal.hedge_notional_usd:,.2f} "
f"exceeds cap ${max_hedge_notional:,.2f}.",
err=True,
)
raise typer.Exit(2)

pol = guard_or_exit(
ACTION_HEDGE,
policy_path=policy_path,
wallet=getattr(hl, "_address", None),
network=network,
market=proposal.profile.cfi_instrument,
notional_usd=proposal.hedge_notional_usd,
)

# Size the order in CFI v2 (BTCSWP) units. SDK rounds to szDecimals.
wire_px = snapshot.oracle_px or proposal.profile.baseline_b0
size = proposal.hedge_notional_usd / wire_px
Expand Down Expand Up @@ -309,6 +341,15 @@ def execute_cmd(
f"{fill.instrument} @ {fill.price} (oid={fill.oid})"
)

if pol is not None and pol.daily_notional_limit_usd is not None:
from cli.session_policy import PolicyCounters
PolicyCounters().record(
getattr(hl, "_address", None),
network,
current_workspace(),
abs(float(fill.quantity) * float(fill.price)),
)

# Persist the HedgeJob.
job_id = f"HEDGE-{int(time.time() * 1000)}"
job = {
Expand All @@ -327,7 +368,7 @@ def execute_cmd(
"status": "active",
"cumulative_savings_usd": 0.0,
"last_sample_at_ms": int(time.time() * 1000),
"network": "mainnet" if mainnet else "testnet",
"network": network,
}
hedges = _load_hedges()
hedges.insert(0, job)
Expand Down
6 changes: 4 additions & 2 deletions cli/commands/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def setup_check():
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 "
"Raw-key mode active. For MCP/agent use, prefer `hl auth import` or hosted Nunchi Auth "
"so the AI client receives scoped access instead of a private key."
)
elif has_keystore:
Expand All @@ -50,14 +50,16 @@ def setup_check():
ok_items.append("HL_KEYSTORE_PASSWORD found in ~/.hl-agent/env")
else:
issues.append("HL_KEYSTORE_PASSWORD not set (needed for auto-unlock)")
elif pairing is not None:
ok_items.append(f"scoped-token signing context found ({pairing.address})")
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."
"NUNCHI_WEB_AUTH_PAIR_TOKEN and NUNCHI_WEB_AUTH_ADDRESS, or run `hl auth import` locally."
)

# 3. Network
Expand Down
2 changes: 2 additions & 0 deletions cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from cli.commands.journal import journal_app
from cli.commands.keys import keys_app
from cli.commands.hedge import hedge_app
from cli.commands.auth import auth_app
from cli.commands.margin import margin_app
from cli.commands.trading import trading_app
from cli.commands.house import house_app
Expand Down Expand Up @@ -66,6 +67,7 @@
app.add_typer(skills_app, name="skills", help="Skill discovery and registry")
app.add_typer(journal_app, name="journal", help="Trade journal — structured position records with reasoning")
app.add_typer(keys_app, name="keys", help="Unified key management across backends")
app.add_typer(auth_app, name="auth", help="Scoped-token auth for keyless local agents")
app.add_typer(hedge_app, name="hedge", help="CFI v2 funding-rate hedge — propose, execute, status, backtest, auto")
app.add_typer(margin_app, name="margin", help="HL collateral — deposits, sub-DEX transfers, isolated margin, auto-topup")
app.add_typer(trading_app, name="trading", help="Trading data surfaces — joined JSON contracts for UIs/bridges")
Expand Down
Loading