Skip to content

v0.2.1: scaled TP, pnl/liquidation alerts, bot fix#2

Merged
Hiksang merged 1 commit intomainfrom
fix/v0.2.1-patches
Mar 10, 2026
Merged

v0.2.1: scaled TP, pnl/liquidation alerts, bot fix#2
Hiksang merged 1 commit intomainfrom
fix/v0.2.1-patches

Conversation

@Hiksang
Copy link
Copy Markdown
Collaborator

@Hiksang Hiksang commented Mar 10, 2026

Summary

  • Scaled Take-Profit (trade scale-tp): 분할익절 — place multiple reduce-only limit orders at different price levels
  • PnL/Liquidation Alerts: alert add -t pnl --loss 50 / -t liquidation --margin-pct 20
  • Bot PLACEHOLDER fix: Remove hardcoded job-id (startJob handles it automatically)
  • limitOrder reduceOnly: Added opts.reduceOnly support to all 3 exchange adapters

Test plan

  • 862 unit tests passing
  • trade scale-tp --help shows correct usage
  • --dry-run trade scale-tp BTC --levels "72000:25,75000:25,80000:50" works
  • alert add --help shows new pnl/liquidation options

🤖 Generated with Claude Code

- Add `trade scale-tp` command: place multiple TP limit orders at
  different price levels (분할익절). Supports --dry-run.
  Example: perp trade scale-tp BTC --levels "72000:25,75000:25,80000:50"
- Add reduceOnly option to limitOrder interface (all 3 exchanges)
- Add pnl/liquidation alert types to alert daemon:
  - `alert add -t pnl --loss 50` — triggers when uPnL drops below -$50
  - `alert add -t pnl --profit 100` — triggers when uPnL exceeds $100
  - `alert add -t liquidation --margin-pct 20` — margin ratio warning
- Fix bot.ts PLACEHOLDER job-id: was hardcoded string, now removed
  (startJob already appends --job-id automatically)
- Bump version to 0.2.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Hiksang Hiksang merged commit 0f21071 into main Mar 10, 2026
@Hiksang Hiksang deleted the fix/v0.2.1-patches branch March 10, 2026 07:28
Hiksang added a commit that referenced this pull request Apr 30, 2026
HypurrQuant_FE CLAUDE.md SSOT rule #2 bans fallback patterns of any
form: live failure must propagate, NOT be silently substituted by
local cache pretending to be authoritative state.

The previous verifyAster implementation returned local cache items
with `source: "local-cache"` + warnings — that's the textbook
"silent substitution" anti-pattern. It hid that live verify isn't
supported behind a synthesized success envelope, exactly the failure
mode the SSOT rule exists to prevent.

Resolution: throw `NOT_IMPLEMENTED` with explicit remediation pointing
to `wallet agent list aster` (the proper local-cache surface). The
aggregate verify command surfaces Aster as an error slot — caller
sees clearly that this DEX has no live verify path.

Code:
- src/commands/agent.ts:58-72 — throw, no synthesized result
- Aggregate handler unchanged: existing slotResult() + error rendering
  already handle thrown PerpError correctly

Tests deleted (obsolete with no-cache return):
- aster local cache verify / agentName not in cache / empty cache
- aster text output renders expired ms
- aggregate text mode includes per-DEX warnings
- aster locally-expired entry

Tests updated:
- aster — throws NOT_IMPLEMENTED with remediation
- aggregate happy path — aster slot is NOT_IMPLEMENTED error
- aggregate partial failure — aster NOT_IMPLEMENTED + lighter coexist
- aster text output prints NOT_IMPLEMENTED + remediation
- timestamp test moved from aster to hyperliquid

1223 unit tests pass.
Hiksang added a commit that referenced this pull request Apr 30, 2026
Per HypurrQuant_FE CLAUDE.md SSOT rule #2: "fallback 은 어떤 형태로든 절대 금지.
실패하면 실패." Removed silent error swallowing and default substitution from
13 critical paths. Errors now propagate; explicit fallback opt-in is preserved
where the fallback is a documented caller-facing behavior.

Tier 1 — silent functional substitution:
- trade-validator: removed .catch defaults on getBalance/getPositions/
  getOrderbook (errors propagate); throw on missing limit price / leverage
- smart-order: flipped fallback default true → false; opt-in only
- position-history: validate side / realizedPnl from stream events; throw
  on malformed payloads instead of defaulting "long" / "0"
- bridge-engine: refuse to fall through to Arbitrum RPC on unknown chain
- lighter-spot: throw on missing nonce / txType from signer
- guardrail/perp-guardrail: fail closed when policy_config missing
- arb-auto / arb: skip Aster symbols missing markPrice / lastFundingRate
  (no more 0-coercion polluting downstream comparisons)

Tier 2 — silent .catch(() => null):
- api/public/{hyperliquid,lighter,pacifica}: removed silent error swallow
  from raw fetchers; callers (arb / arb-auto) now use Promise.allSettled
  with explicit stderr logging per failed DEX

Tests:
- updated 5 trade-validator tests to assert error propagation (was: silent
  default behavior)
- updated 4 smart-order tests to use explicit { fallback: true } where the
  fallback path is intentionally exercised
- added 3 new tests verifying default-throws behavior (no asks / no bids /
  IOC reject)
- 1227 tests pass; build clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request Apr 30, 2026
…logging

Two helpers extracted to remove repeated boilerplate the SSOT-rule-#2
fixes introduced:

- src/api/public/_http.ts: assertOk(res, label). Replaces 7 copies of the
  "throw on non-2xx with body excerpt" guard across pacifica.ts (×2),
  hyperliquid.ts (×1 hlPost), lighter.ts (×4).

- src/utils.ts: logSettledRejections(settled, labels, prefix). Replaces 5
  copies of the "log each rejected outcome to stderr" loop across arb.ts
  (×2), arb-auto.ts (×1), arb/index.ts (×1), strategies/funding-arb.ts (×1).

Behavior preserved (1230/1230 tests still pass). The dashboard/ws-feeds.ts
allSettled site is intentionally untouched — it silently swallows feed
failures by design (REST fallback covers it) and is out of scope for this
SSOT pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
- New `docs/SSOT_RULES.md` codifies Single Secret Source rule alongside
  Rule #2 (no fallback). All cryptographic secrets live in exactly one
  persistent location with one consistent format.
- Lighter L2 slot private key migrates from `~/.perp/.env LIGHTER_API_KEY`
  plaintext to AES-256-GCM-encrypted keystore at
  `~/.perp/lighter-agents/<account>-<slot>.json` (mode 0600).
  New module `src/agent-wallet/lighter-keystore.ts` (save/load/delete +
  one-time `migrateFromEnvIfPresent` for legacy users).
- Adapter init reads via `loadLighterKey()`; constructor no longer reads
  any LIGHTER_* env vars. Migration helper auto-clears legacy `.env`
  entries on first run.
- `wallet show --json` now exposes `owsActive: { name, evmAddress,
  solanaAddress }` so agents can discover the master EVM/Solana address
  without env-derived fallbacks. Closes the friction surfaced by
  trader-persona-alpha v1.
- `src/index.ts` removes the no-arg `dotenv.config()` call that
  silently auto-loaded any CWD `.env`. Only `~/.perp/.env` loads now,
  eliminating a Rule #3 violation surface where stray dev keys polluted
  master-address resolution.
- Tests: 1230 -> 1249 (+19 keystore tests). Build green. Live signed-read
  smoke on all 4 DEXs (HL/PAC/LT/Aster) confirms keystore-driven
  Lighter signer initializes end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
The four referral apply sites in src/index.ts had .catch blocks that wrote
referralApplied[exchange]=true even when the venue rejected the call. This
violated SSOT Rule #2 (no silent fallback) — failure became indistinguishable
from success in settings.json, blocking retry and hiding venue rejection
(e.g. HL first-write-wins when a different referrer was already registered,
or LT auth/network failures).

The .catch handlers now only log the failure to stderr; settings stays
unmodified so the next adapter init retries. Reapply still gates on
referralApplied=false, so successful applies still lock once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
… remediation

When `wallet agent approve` failed because a venue rejected the request
(e.g. HL "Must deposit before performing actions", Aster "No aster user
found"), the locally-generated `agent-<dex>-<master>` OWS wallet was
left orphaned in `~/.ows/wallets/`. Retry hit "wallet name already
exists" and the prior remediation only printed the agent address, not
the cleanup command.

This commit auto-deletes the orphaned local wallet on pre-venue
rejection (HL/PAC/Aster — Lighter uses a keystore, not OWS wallet, so
skipped). Post-venue / persist failures still leave a status:"partial"
record so the user can revoke the venue-side registration.

The new remediation is actionable: "Local agent wallet cleaned up.
Deposit funds and retry: perp wallet agent approve <dex> --master
<master>". Agent address still appears in the message body for
diagnostics. Per SSOT Rule #2, rollback failures do NOT mask the
original error — failures are logged to stderr and the original error
propagates to the caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
Plan v3.0 documented "HMAC removed entirely in v0.12 — no
soft-deprecation" but only the runtime/signing path was removed. The
setup wizard, EXCHANGE_ENV_MAP, and `wallet set aster <key>` surfaces
kept the legacy `chain: "apikey"` entry, so new users following the
wizard still got prompted for ASTER_API_KEY/ASTER_API_SECRET — values
that no longer drive any signing path. Confusing UX surfaced during
v0.12.3 clean-state Docker QA.

Removes:
- `EXCHANGE_ENV_MAP.aster` entry (init.ts)
- `EXCHANGE_PK_ENV_VARS.aster` entry (config.ts)
- The setup wizard's Aster API key prompt (replaced with redirect to
  `wallet agent approve aster`)

Adds:
- `wallet set aster <key>` now throws INVALID_PARAMS with remediation
  pointing at `wallet agent approve aster`. Failing loudly per SSOT
  Rule #2.

The new path: every Aster user must onboard via
`perp wallet agent approve aster --master <wallet>`. Same as HL/PAC/LT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
 catch removal

Replaces the !this._dex heuristic in getBalance() with an explicit query
to HL info type "userAbstraction". Returns one of:
- "unifiedAccount"   → "unified"  (spot USDC = true equity)
- "portfolioMargin"  → "portfolio" (treated like unified for now)
- "disabled"         → "standard" (perp clearinghouse = truth)

Discovered during v0.12.6 Docker QA: existing branch assumed every
mainnet (non-HIP-3) user was unified. Standard-mode users (required for
builder fee accrual) silently received unified accounting → wrong equity
when their actual collateral lived on the perp side.

The previous silent catch fallback (spot-fail → perp values) violated
SSOT Rule #2 — masked the mode mismatch instead of failing loudly.
Removed; venue-side fetch errors now propagate.

New helper:
- `HyperliquidAdapter._getAbstractionMode()` (cached via TTL_ACCOUNT)

Cache key: `acct:hl:abstraction:<address>`. HIP-3 dex accounts skip
the lookup since cross margin scopes per-DEX in standard semantics.

SKILL.md auto-synced to 0.12.7 via prepublish hook.

Refs: HL docs `trading/account-abstraction-modes.md` for the 4 modes
and the userSetAbstraction action shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
Two small UX/docs fixes from v0.12.7 clean-state Docker QA:

(#11) `perp account balance` was renamed to `perp portfolio` in v0.12,
but typing the old command returned "unknown command" with no pointer.
Adds an explicit redirect subcommand that prints the correct path and
exits 1 (per SSOT Rule #2: don't silently retarget — tell the user what
to run).

(#15) New `CHANGELOG.md` (Keep a Changelog format). Documents v0.12.0
through v0.12.7 plus orphan v0.12.4 tag. Future releases auto-link
back via the bottom version table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
…bstraction)

Adds CLI surface for setting Hyperliquid account abstraction mode:
  perp wallet manage account-mode <unified|standard|portfolio> [--master <w>] [--passphrase <pp>] [--json]
  perp wallet manage account-mode                # no arg → show current mode

Reuses the existing EIP-712 master-signing infra from runHlApproveFlow:
same HyperliquidSignTransaction domain (chainId 42161), same OwsEvmSigner
and signTypedData plumbing — only the type struct (UserSetAbstraction)
and action payload differ. Mode strings map unified→unifiedAccount,
standard→disabled, portfolio→portfolioMargin.

Read side reuses HyperliquidAdapter._getAbstractionMode() (added v0.12.7);
adds a tiny setAddress helper so the show branch can scope the read to a
master OWS wallet without unlocking it.

Per Rule #2: no fallbacks. Best-effort prev-mode capture is metadata-only
and never masks the write outcome. Signing/parse/venue failures throw
PerpError with structured remediation.
Hiksang added a commit that referenced this pull request May 1, 2026
…#2)

aster.ts signed GET/DELETE used _handleResponse() which only checks HTTP
status; only POST validated `json.code/msg` via _handleEip712Response().
Aster's API commonly returns HTTP 200 + {code, msg: "..."} for signing
or auth failures, so getBalance() silently cached zero balances on
errors — masking the underlying signature failure and violating SSOT
Rule #2 (no silent fallback).

Unify under _handleAsterResponse(res): validate HTTP status AND parse
the JSON error envelope, throw structured PerpError with venue message
+ remediation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
Aggregate fix from Codex independent review (artifact .omc/artifacts/ask/
codex-review-perp-cli-v0-12-0-v0-12-10-...md). 7 atomic commits:

- 7a7c594 fix(aster): Tier 2/3 signer model — separate user vs signer per V3 spec
- dd85a96 fix(aster): validate venue JSON error codes on signed GET/DELETE (Rule #2)
- 5a3cdfb fix(hyperliquid): handle dexAbstraction mode + portfolio non-USDC awareness
- e5fa570 fix(passphrase): wallet key create — apply optsWithGlobals (parent shadow)
- d105f54 fix(setup): landing-page check — recognize ASTER_PRIVATE_KEY
- 9593cd3 fix(aster): testnet chainId=714 support
- 1a0a461 chore: drop stale env-key fallback comment in manage.ts

Tests 1260 → 1281 (+21). Build green.

The first two HIGH commits target the actual root cause of "Signature
check failed" observed during v0.12.10 Docker QA — Aster query fields
needed user/signer split + GET/DELETE response validator was bypassed
silently for non-200 venue codes wrapped in HTTP 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
…gning

v0.12.11's user/signer split was correct syntax but didn't address the
venue's actual rule: per Aster V3 spec, `signer` MUST be a registered
API_WALLET (agent). Master self-signing (user==signer==master) is
rejected with `code:-1000 msg:"Signature check failed"`, even though
the EIP-712 envelope is well-formed.

Reference: HypurrQuant_FE's AsterPerpAdapter (canonical reference at
packages/core/defi/perp/adapters/AsterPerpAdapter.ts:773-775) throws if
agent isn't configured; never attempts master self-signing.

Tier 2 (OWS master) and Tier 3 (PK direct) now throw NOT_SUPPORTED with
remediation pointing at `wallet agent approve aster`. Tier 1 (agent)
unchanged. SSOT Rule #2 compliant: explicit failure beats silent venue
rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
…rom v0.12.11)

Codex re-review of v0.12.11 found that unifying GET/DELETE through
_handleAsterResponse() also dropped the prior multi-attempt 429 retry
loop — leaving signed reads single-shot under rate-limit pressure.

Restored: up to 3 attempts with exponential backoff (2s/4s/8s) and a
fresh _buildSignedQueryString per attempt (Aster reuses recent nonces
strictly, so reusing a stale signature on retry would itself be
rejected). Non-429 venue errors still throw immediately via
_handleAsterResponse — no silent fallback (Rule #2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 1, 2026
The program-level .catch handler at the bottom of index.ts always
serialized errors as code:"FATAL" with no remediation, even when the
underlying error was a typed PerpError carrying explicit code and
remediation. v0.12.13's classifyError fix preserved typed errors only
through withJsonErrors wrappers; errors that propagated past withJsonErrors
to the program top-level still got downgraded.

Symptom (live in v0.12.14): `perp -e aster account positions --json`
without an agent emitted
  {"code":"FATAL","message":"Aster requires a registered agent..."}
instead of the typed
  {"code":"NOT_IMPLEMENTED","status":501,"retryable":false,
   "remediation":"perp wallet agent approve aster --master <wallet>",
   "message":"Aster requires a registered agent..."}

The top-level catch now branches on `err instanceof PerpError` and
forwards the structured payload through jsonError. Verified live in
Docker: NOT_IMPLEMENTED + remediation now appears as expected.

SSOT Rule #2: machine consumers receive the correct semantic code +
actionable remediation rather than a generic FATAL stub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 2, 2026
Codex v0.12.16 verdict flagged the asterAgentMissing helper's try/catch
silenced settings-corrupt failures by returning false (= 'agent IS
present'). That violated SSOT Rule #2 and could leave a real broken-
settings user staring at a generic red dash with no clue.

Drops the silent catch — listAgents() is a file read; if it throws, let
the outer landing catch handle the message. Adds a unit test asserting
'agent required' renders only when settings actually shows no aster
agent + adapter call failed, distinct from venue-down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 2, 2026
…r agent-required Rule #2)

- account.ts: HIGH regression — twap-orders catch wrapped getAdapter() AND pac(), then
  unconditionally returned NOT_SUPPORTED + Pacifica remediation. Mislabeled unrelated
  failures (network, locked wallet, missing PK, typed PerpError). Now uses explicit
  hasPacificaSdk() guard so only the Pacifica-only assertion is rewritten; other
  errors propagate to the standard classifier.

- landing.ts + index.ts: MED Rule #2 gap — agent-required hint now requires both
  (a) local Aster agent absent AND (b) errorCode in {NOT_IMPLEMENTED, NO_SIGNER_AVAILABLE,
  AGENT_EXPIRED}. Generic Aster outages no longer render 'agent required'.

- landing.test.ts: +2 regression cases (network-error path, AGENT_EXPIRED/NO_SIGNER path).
  Tests 1305 → 1307.

- bump 0.12.17 → 0.12.18 (package.json, marketplace.json, SKILL.md, CHANGELOG).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 3, 2026
Adds new asset class: HL outcome markets (binary/range contracts, fully
collateralized, no leverage, no liquidation, USDH-quoted, $10 min order).
Currently 1 live market: BTC binary daily settling at 06:00 UTC.

New surface:
  perp outcome list                              # active markets
  perp outcome book <outcome> <side> [--depth N] # orderbook
  perp outcome positions                         # holdings
  perp outcome orders                            # open orders
  perp outcome buy <outcome> <side> <usd>        # IoC by default, --limit for GTC
  perp outcome sell <outcome> <side> <usd>
  perp outcome cancel <outcome> <side> <oid>

Key facts (verified end-to-end against mainnet):
- Asset id formula: 100,000,000 + (10 * outcome + side)
- Coin name: '#<enc>' (l2Book/candle/allMids), '+<enc>' (spot balance)
- Universe: POST /info {type:"outcomeMeta"}
- Positions: filter spotClearinghouseState for '+<enc>' coins
- Order/cancel actions identical to spot, only assetId differs
- Quote token = USDH (NOT USDC) — outcome trades draw from spot USDH balance
- Min notional: price * size >= 10 USDH
- Probe scripts: scripts/probe-outcome-{ws,order}.ts (manual mainnet verify)

Adapter (HyperliquidOutcomeAdapter) composes with HyperliquidAdapter for
signing. Bypasses HL's cached spot state on getPositions() to surface
fresh balances after fills (cache TTL would be stale).

CLI uses optsWithGlobals() so the parent program's --dry-run flag is not
shadowed by the subcommand's local options (commander v13 behavior).

Tests: +10 unit tests covering encoding, coin formatting, description
parsing. Total: 1307 → 1317.

SSOT compliance:
- Rule #2: unknown outcome/side throws SYMBOL_NOT_FOUND/INVALID_PARAMS;
  notional below $10 throws INVALID_PARAMS with remediation.
- Rule #3: no new env vars, reuses existing HL agent.

Out of scope for this commit (deferred):
- Portfolio aggregation of outcome holdings
- Landing page outcome line
- close subcommand (use sell with --limit + notional for now)
- HIP-4 builder/deployer mechanics

Plan reference: .omc/plans/v0.13.0-outcome.md (local).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hiksang added a commit that referenced this pull request May 3, 2026
Codex re-review #2 found one MED regression in the new USDH pre-check.
HyperliquidAdapter._address is empty in agent-only setups (no master pk
in OWS) because setAgentSigner does not propagate the address. The
pre-check returned 0 USDH in that case, producing a false-positive
INSUFFICIENT_BALANCE that blocked legitimate buys.

Now both the pre-check and getPositions resolve the user address via
_resolveUserAddress: prefer _hl.address, fall back to the agent meta's
userEvmAddress in the OWS-stored agent registry, and only throw
NO_SIGNER_AVAILABLE if neither is available. No silent zero substitution
(Rule #2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant