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
20 changes: 14 additions & 6 deletions cli/commands/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,28 @@ def trade_cmd(

instrument = resolve_instrument(instrument)
cfg = TradingConfig()
private_key = cfg.get_private_key()

raw_hl = HLProxy(private_key=private_key, testnet=not mainnet)
hl = DirectHLProxy(raw_hl)
network = "mainnet" if mainnet else "testnet"
if price <= 0 and dry_run:
typer.echo("Dry run requires an explicit --price when skipping market lookup.", err=True)
raise typer.Exit(1)

# If no price given, use mid from snapshot
if price <= 0:
private_key = cfg.get_private_key()
raw_hl = HLProxy(private_key=private_key, testnet=not mainnet)
hl = DirectHLProxy(raw_hl)
snap = hl.get_snapshot(instrument)
if snap.mid_price <= 0:
typer.echo("Error: could not fetch market data for price", err=True)
raise typer.Exit(1)
# For IOC: use mid + slippage
if side.lower() == "buy":
price = round(snap.ask * 1.001, 4)
else:
price = round(snap.bid * 0.999, 4)
typer.echo(f"Using market price: {price}")
else:
hl = None

network = "mainnet" if mainnet else "testnet"
notional_usd = abs(size * price)
notional_cap = cfg.max_notional_usd if max_notional_usd is None else max_notional_usd
if notional_cap <= 0:
Expand All @@ -132,6 +135,11 @@ def trade_cmd(
typer.echo("Dry run: order not submitted.")
return

if hl is None:
private_key = cfg.get_private_key()
raw_hl = HLProxy(private_key=private_key, testnet=not mainnet)
hl = DirectHLProxy(raw_hl)

_confirm_trade(yes)

fill = hl.place_order(
Expand Down
62 changes: 62 additions & 0 deletions configs/presets/jump.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# HOUSE-Jump preset
# ------------------
# Bounded-pilot configuration for the Nunchi HOUSE × Jump Builder Code pilot.
# See: ~/obsidian-vault/projects/2026-04-30-jump-builder-code-pilot-onepager.md
#
# Source it like a normal env file:
# set -a && source ~/agent-cli/configs/presets/jump.env && set +a
# Or hand it to docker / systemd as an env_file.
#
# Required to override at install time per institution:
# - HL_KEYSTORE_PASSWORD or HL_PRIVATE_KEY (your own credential)
# - SETTLEMENT_ADDRESS (the institution's USDC remittance wallet)
#
# Anything below is the HOUSE-Jump default.

# ---- Network ----
# Commented out by default so your shell HL_TESTNET (commonly =true for local
# dev) wins. Uncomment to force mainnet for the actual pilot.
# HL_TESTNET=false

# ---- Builder Code attribution ----
# HOUSE builder address — Nunchi's order-builder wallet that receives the BC surcharge.
BUILDER_ADDRESS=0x0D1DB1C800184A203915757BbbC0ee3A8E12FfB0

# 10 = 1.0 bp surcharge per fill (HL ceiling is 100 = 10 bp).
# Pilot per the v10 one-pager: 1.0 bp cap, 100% remitted to Jump weekly.
BUILDER_FEE_TENTHS_BPS=10

# ---- Settlement (institution-side) ----
# Override per institution. Empty = same wallet does trading + receives remittance.
# For Jump: set this to the USDC receive-wallet Jump designates.
SETTLEMENT_ADDRESS=

# ---- Market focus: TradeXYZ HIP-3 commodity / index / RWA perps ----
# RADAR + PULSE will only consider these markets when scanning.
# Glob patterns are supported; xyz:* picks up any new TradeXYZ HIP-3 listing.
MARKET_WHITELIST=xyz:GOLD,xyz:CL,xyz:SILVER,xyz:XYZ100

# ---- Default strategy ----
# engine_mm = composite-FV quoting engine (4-signal blend, dynamic spreads, ladder).
HOUSE_DEFAULT_STRATEGY=engine_mm

# ---- Risk caps ----
# Max notional per agent (USD). The Fleet Supervisor enforces this per spawned process.
HOUSE_MAX_NOTIONAL_PER_AGENT=250000

# Max drawdown before HouseRiskManager halts the agent (in basis points).
HOUSE_MAX_DRAWDOWN_BPS=300

# ---- Pilot bookkeeping ----
HOUSE_PILOT_NAME=jump
HOUSE_PILOT_DURATION_WEEKS=4
HOUSE_PILOT_SUCCESS_ADV_USD=30000000

# ---- Optional: PULSE → autoresearch auto-launch (D7) ----
# Uncomment to have PULSE fire on FIRST_JUMP / CONTRIB_EXPLOSION signals
# directly into ECC's Strategy Lab so a new strategy starts iterating
# automatically when a HIP-3 pair lights up.
# PULSE_WEBHOOK_URL=http://localhost:4200/api/lab/launch
# PULSE_WEBHOOK_MIN_CONFIDENCE=95
# PULSE_WEBHOOK_TIMEOUT_SEC=3
# PULSE_WEBHOOK_AUTH_TOKEN=
54 changes: 54 additions & 0 deletions docs/agent-cli-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Agent CLI Audit

Validated: 2026-06-23T15:33:05Z

## Scope

This audit checked the local `agent-cli` branch for:

- direct Railway one-click deploy links that bypass `auth.nunchi.trade`
- public HTTP/API control surfaces
- root, OpenClaw, and Hermes Railway deploy paths that could be used outside web-auth
- lightweight CLI/test health
- README drift against the current branch

## Working

- The local editable package is installed and imports under the active environment.
- `python3 -m cli.main strategies` renders the current strategy catalog: 18 strategies and 3 YEX markets.
- `python3 -m cli.main setup check` runs and confirms the Hyperliquid SDK, testnet config, builder fee config, and data directory.
- `python3 -m cli.main run avellaneda_mm --mock --max-ticks 1` completes successfully and places mock orders.
- `python3 -m cli.main apex run --mock --max-ticks 1` completes successfully after the startup logging format fix in this pass.
- All 18 registered strategies import cleanly and complete a one-tick isolated mock smoke run.
- Focused auth/web-auth tests passed: `tests/test_entrypoint.py`, `tests/test_config.py`, `tests/test_web_auth.py`, and `tests/test_pair_money_cli.py`.
- Full local suite passed under the available shell: `1317 passed, 3 warnings`.
- Hosted-agent runtime entrypoints compile without the removed public deploy templates.

## Broken Or Risky Before This Pass

- `README.md` published direct public Railway template links. These allowed one-click deployment straight from GitHub without going through `auth.nunchi.trade`, so subscription and lifecycle checks could be bypassed.
- `deploy/openclaw-railway/src/server.js` and `deploy/hermes-railway/src/server.js` exposed `/api/pause`, `/api/resume`, and `/api/configure` without auth. Those are mutating control endpoints.
- `scripts/entrypoint.py` had optional auth for the same mutating control endpoints: if `API_AUTH_TOKEN` was unset, the endpoints were open.
- `deploy/openclaw-railway/Dockerfile` and `deploy/hermes-railway/Dockerfile` used `COPY ../../ .` while their Railway configs did not pin `dockerfilePath`. That is fragile because Docker cannot copy files outside the build context.
- `docs/api-reference.md` stated all endpoints were unauthenticated and documented pause/resume without auth headers.
- `README.md` still claimed 14 strategies and 16/13 MCP tools while the current branch exposes 18 strategies and 23 MCP tools.
- `hl jobs` was registered as a live CLI group, but it was a design-only skeleton: most commands printed `Not yet implemented — design PR only`, and the backing engines/events/custody modules raised `NotImplementedError`.
- The active local `python3` is 3.9.6 while `pyproject.toml` requires `>=3.10`; release validation should use the hosted-agent runtime environment.

## Remediated

- `README.md` now routes managed launch through `https://auth.nunchi.trade` and no longer contains public `railway.com/new/template` links or Railway button badges.
- `scripts/entrypoint.py` now fails closed for mutating control endpoints unless `API_AUTH_TOKEN` is set.
- Node and Python CORS allow `X-API-Token` as an alternate token header.
- `docs/api-reference.md` now distinguishes unauthenticated read endpoints from token-required mutating control endpoints.
- Public Docker/Railway deployment files were removed: root `Dockerfile`, root `railway.toml`, and the `deploy/openclaw-railway` / `deploy/hermes-railway` templates.
- `tests/test_deploy_policy.py` prevents direct Railway template links and public Docker/Railway deployment configs from returning.
- `tests/test_entrypoint.py` covers fail-closed control auth and `X-API-Token`.
- `tests/test_logging_config.py` covers the startup currency formatting that previously emitted logging errors.
- `README.md` now reflects the observed branch counts: 18 strategies, 23 MCP tools, and 1,317 passing tests.
- The nonfunctional `hl jobs` skeleton and its stale design spec were removed so the CLI no longer advertises commands that cannot run.

## Remaining Follow-Ups

- Re-run the full suite under Python 3.10+ or the hosted-agent runtime before release. The local shell only exposed Python 3.9.6, even though all tests passed there.
- If `auth.nunchi.trade` has a more specific hosted-agent route than the root URL, update the README launch link to that canonical URL.
53 changes: 53 additions & 0 deletions modules/market_whitelist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""MarketWhitelist — shared filter for restricting scanners to a subset of HL perps.

Used by RADAR and PULSE to focus on a configured asset universe (e.g. TradeXYZ
HIP-3 commodity / index perps for the HOUSE-Jump preset).

Patterns:
- Empty list = pass through (no filtering, scan all assets)
- Exact name match: ``xyz:GOLD``
- Glob pattern: ``xyz:*`` matches any TradeXYZ HIP-3 asset
- Comma-separated env var: ``MARKET_WHITELIST=xyz:GOLD,xyz:CL,xyz:SILVER``
"""
from __future__ import annotations

import fnmatch
import os
from dataclasses import dataclass, field
from typing import List


@dataclass
class MarketWhitelist:
"""Glob-aware asset filter."""

patterns: List[str] = field(default_factory=list)

@property
def active(self) -> bool:
"""True when the whitelist is non-empty and should restrict the universe."""
return bool(self.patterns)

def matches(self, asset_name: str) -> bool:
"""Return True when the asset is allowed.

Empty whitelist = allow everything (back-compat). Otherwise the asset
must match at least one pattern via shell-style glob (``fnmatch``).
"""
if not self.patterns:
return True
return any(fnmatch.fnmatchcase(asset_name, p) for p in self.patterns)

@classmethod
def from_env(cls, env_var: str = "MARKET_WHITELIST") -> "MarketWhitelist":
"""Parse comma-separated patterns from an env var. Whitespace tolerant."""
raw = os.environ.get(env_var, "").strip()
if not raw:
return cls()
patterns = [p.strip() for p in raw.split(",") if p.strip()]
return cls(patterns=patterns)

@classmethod
def from_list(cls, patterns: List[str]) -> "MarketWhitelist":
"""Build from an explicit list (e.g. preset YAML)."""
return cls(patterns=[p for p in patterns if p])
86 changes: 86 additions & 0 deletions modules/openrouter_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Shared OpenRouter/OpenAI usage parsing for cost experiments."""
from __future__ import annotations

from typing import Any, Dict, Optional


def usage_value(usage: Any, *names: str) -> int:
if usage is None:
return 0
for name in names:
value = getattr(usage, name, None)
if value is not None:
return int(value or 0)
data: Dict[str, Any] = {}
if hasattr(usage, "model_dump"):
try:
data = usage.model_dump() or {}
except Exception:
data = {}
for name in names:
if name in data:
return int(data.get(name) or 0)
return 0


def usage_cost(usage: Any) -> Optional[object]:
if usage is None:
return None
cost = getattr(usage, "cost", None)
if cost is not None:
return cost
extra = getattr(usage, "model_extra", None) or {}
if isinstance(extra, dict) and extra.get("cost") is not None:
return extra.get("cost")
if hasattr(usage, "model_dump"):
return usage.model_dump().get("cost")
return None


def extract_cache_metrics(usage: Any, *, input_tokens: int) -> Dict[str, Any]:
if usage is None:
return {}

usage_data: Dict[str, Any] = {}
if hasattr(usage, "model_dump"):
try:
usage_data = usage.model_dump() or {}
except Exception:
usage_data = {}

def get_value(name: str) -> Any:
if hasattr(usage, name):
return getattr(usage, name)
return usage_data.get(name)

prompt_details = get_value("prompt_tokens_details") or usage_data.get("prompt_tokens_details") or {}
if hasattr(prompt_details, "model_dump"):
prompt_details = prompt_details.model_dump()
elif not isinstance(prompt_details, dict):
prompt_details = {
"cached_tokens": getattr(prompt_details, "cached_tokens", None),
}

cache_read = get_value("cache_read_input_tokens")
cache_creation = get_value("cache_creation_input_tokens")
cached_tokens = prompt_details.get("cached_tokens")
cache_savings = get_value("cache_savings_usd") or get_value("cache_discount_usd")

if cache_read is None and cached_tokens is None and cache_creation is None and cache_savings is None:
return {}

cache_read_int = int(cache_read or 0)
cache_creation_int = int(cache_creation or 0)
cached_int = int(cached_tokens if cached_tokens is not None else cache_read_int)
uncached_input_tokens = max(0, int(input_tokens or 0) - cached_int)
cache_hit_rate = (cached_int / input_tokens) if input_tokens else 0.0
metrics: Dict[str, Any] = {
"cache_read_input_tokens": cache_read_int,
"cache_creation_input_tokens": cache_creation_int,
"cached_tokens": cached_int,
"uncached_input_tokens": uncached_input_tokens,
"cache_hit_rate": round(cache_hit_rate, 6),
}
if cache_savings is not None:
metrics["cache_savings_usd"] = cache_savings
return metrics
Loading