Skip to content

Add PAPER trading mode + fix partial-fill close orphaning positions - #1

Open
Freecey wants to merge 3 commits into
Julian-dev28:mainfrom
Freecey:feat/paper-mode-and-safety-fixes
Open

Add PAPER trading mode + fix partial-fill close orphaning positions#1
Freecey wants to merge 3 commits into
Julian-dev28:mainfrom
Freecey:feat/paper-mode-and-safety-fixes

Conversation

@Freecey

@Freecey Freecey commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Three changes in three commits (happy to split into separate PRs if you prefer):

1. feat(paper): PAPER mode — simulated fills against live market prices

There is currently no step between OFF (observe only) and LIVE (real money). This adds "mode": "PAPER": the full pipeline (scan → TA filter → AI research → risk gates → DSL exits) runs unchanged against live market data, but every authenticated exchange call routes to a simulated book (client/paper_engine.py):

  • fills at the live L2 touch ± paper_slippage_bps, taker fees at paper_fee_bps
  • position netting with realized PnL, reduce-only clamped to live size (never flips), pre-trade margin check
  • virtual SL/TP triggers evaluated against live mids on every account-state read
  • state persisted atomically to .paper-state.json (survives daemon restarts, same pattern as .dsl-state.json)
  • fetch_account_state returns the virtual book in HL's exact clearinghouseState shape, so the gates, DSL engine, dashboard and status.py work without modification
  • works with no HYPERLIQUID_* env vars — useful for new users evaluating a config before funding a wallet

Known approximations are documented in the module docstring and README: orders always fill in full (no book-depth exhaustion) and triggers fill at their trigger price (real markets gap) — so paper results are slightly optimistic by construction.

2. fix(exits): partial-fill close could orphan a position with no stop

The important one. In close_position_market, the DSL tracker was deregistered as soon as the reduce-only IOC returned ok=True, without checking totalSz against the requested size. On a thin book, a partial fill left the residual position live on the exchange with no stop, no floor, no timeout — exactly the failure mode this engine exists to prevent.

Now: totalSz is reconciled against the requested size, the residual is retried up to 3× (fresh size and price each pass), and if any size still remains the tracker is kept and the call returns ok=False with partial/remaining_sz details so the next DSL tick re-fires the close. Realized PnL uses the size-weighted fill price across all fills.

Also in this commit:

  • rehydrate_from_exchange stamped synthesized trackers with entry_time=now, re-arming hard_timeout_minutes from scratch on every restart (a 2h50m-old position got a fresh 3h lease). It now looks up the real open time from the account's fill history (most recent open-from-flat fill), falling back to now when unavailable.
  • backup SL/TP placement is retried once (the typical failure is a transient meta-cache/429 flake), and a persistent SL failure emits an error event to the session feed instead of only a log line — the position is otherwise running with the 60s DSL loop as its only stop and nobody knows.
  • gate inputs: open-position notional now uses HL's positionValue (live mark) instead of |szi| × the stale analysis entry price.
  • place_hl_trigger_order callers pass is_long_position= as an explicit keyword — the positional is_buy only worked because entry direction happens to equal position side.

3. fix(security): bind the API server to localhost by default

The FastAPI server (whose operator endpoints place/close real trades behind a single static token) listened on 0.0.0.0 unconditionally. Default is now 127.0.0.1, overridable via HERMES_HOST; the Dockerfile, fly.toml and the k8s configmap set HERMES_HOST=0.0.0.0 so containerized deploys behave exactly as before.

Testing

  • 193 offline tests pass: the 173 existing ones (no regressions) + tests/test_paper.py (14 tests: fills, netting, reduce-only clamp, margin check, trigger firing, persistence, exchange-layer routing) + tests/test_close_and_rehydrate_fixes.py (6 regression tests: partial close keeps the tracker, residual retry, weighted fill price, dust handling, open-time from fill history, API-failure fallback).
  • End-to-end PAPER smoke test against the live API with no credentials: entry filled at the real L2 touch, equity marked to live mids, virtual SL registered, clean close, state file intact.

🤖 Generated with Claude Code

Freecey and others added 3 commits June 12, 2026 21:55
New mode between OFF and LIVE: the full pipeline (scan, TA, AI research,
risk gates, DSL exits) runs unchanged against live data, but every
authenticated exchange call routes to a simulated book
(client/paper_engine.py):

- fills at the live L2 touch + paper_slippage_bps, taker fees at
  paper_fee_bps, position netting with realized PnL
- reduce-only clamps to the live size (never flips), pre-trade margin check
- virtual SL/TP triggers evaluated against live mids on every account read
- state persisted atomically to .paper-state.json across restarts
- fetch_account_state returns the virtual book in HL's exact
  clearinghouseState shape, so gates/DSL/dashboard need no changes
- no HYPERLIQUID_* env vars required (resolve_user_address -> "paper")

Loop startup now logs the real configured mode (was hardcoded "LIVE").
Covered by tests/test_paper.py (14 offline tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lience

- close_position_market: an IOC reduce-only close can PARTIALLY fill on a
  thin book; the code deregistered the DSL tracker on ok=True without
  checking totalSz, orphaning the residual position with no stop/floor/
  timeout. Now reconciles totalSz vs requested, retries the residual up to
  3x (fresh size + price each pass), keeps the tracker and returns
  ok=False + partial details if any size remains (next DSL tick re-fires).
  Realized PnL uses the size-weighted fill price across all fills.

- rehydrate_from_exchange: synthesized trackers stamped entry_time=now,
  re-arming hard_timeout_minutes from scratch on every restart. Now looks
  up the real open time from the account's fill history (most recent
  open-from-flat fill), falling back to now when unavailable.

- backup SL/TP triggers: placement is retried once (typical failure is a
  transient meta-cache/429 flake) and a persistent SL failure now emits an
  error event to the session feed instead of only a log line.

- gate inputs: open-position notional now uses HL's positionValue (live
  mark) instead of size x the stale analysis entry price.

- place_hl_trigger_order callers pass is_long_position= explicitly (the
  positional is_buy only worked because entry direction == position side).

- maybe_execute no longer requires HYPERLIQUID_PRIVATE_KEY in PAPER mode.

Covered by tests/test_close_and_rehydrate_fixes.py (6 regression tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FastAPI server (dashboard + operator endpoints that place/close real
trades behind a single static token) listened on 0.0.0.0 unconditionally.
Default is now 127.0.0.1, overridable via HERMES_HOST. Containerized
deploys keep their behavior: HERMES_HOST=0.0.0.0 is set in the Dockerfile,
fly.toml and the k8s configmap, where the platform provides the network
isolation. Dockerfile also pins HERMES_PAPER_STATE_FILE onto the /data
volume alongside the other state files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Freecey Freecey mentioned this pull request Jun 12, 2026
Julian-dev28 added a commit that referenced this pull request Jul 12, 2026
…lidated

Pre-registered honest implementations of all 7 visible '25/25 leaderboard'
strategy families, our data (90 coins daily + 40 coins hourly 208d), net
25bps+funding, frozen thresholds, matched nulls >=2000, Bonferroni 0.0056:
6 REFUTED, 1 UNDERSAMPLED (dominance_transition, n=20). Best null p
anywhere: 0.215. Their #1 (claimed Sharpe 7.07 / 2,311%) earns -0.57%/trade
OOS here and loses to random same-coin timing in 2000/2000 resamples; their
claimed n for dominance signals is arithmetically impossible (9-21 fires in
5.5y). W-Q verdict upgrades: presumptively overfit -> empirically
contradicted. Nothing ships, no recorder owed. 12MB hourly cache gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Julian-dev28 added a commit that referenced this pull request Jul 22, 2026
…piece #1

W-MATH2: we run ~2.87 effective bets, not 15; the 4 xyz-short books are ~1.28
bets held four ways (mean pairwise corr 0.83), and 7 xyz shorts stopped TOGETHER
for -$8.59 on 07-21 exactly as the correlation predicts. W-MATH3 independently
promoted a per-sector short cap as THE fix (not momentum timing).

- xyz_short_concentration_gate: caps concurrent DISTINCT xyz-equity short names
  (<=3) AND their combined notional (<=25% equity). Name cap is the hard control;
  notional cap is best-effort (skipped if positions carry no value field, so a
  missing field never wrongly blocks). Only gates NEW xyz-equity shorts.
- wired into eval_all_gates NOT carveout-exempt — the whole point is the
  correlated BOOK cluster, so strategy books obey it too.
- config: max_xyz_short_names=3, max_xyz_short_notional_pct=0.25 (W-MATH2's number;
  was ~7 names / ~124% equity on overnight).
- 4 tests (name cap, notional cap, ignores longs/crypto/missing-value, wired +
  not-carveout-exempt). 27 book-path + wiring tests green, no collision.

Honest caveat (W-MATH2): nothing live is edge-proven yet (n<8); this cap is
risk-control, and the allocation is variance-driven until books hit n>=8 (~1-2wk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PsychoLogicAu referenced this pull request in PsychoLogicAu/hermes-trader Aug 20, 2026
…nked)

7e63882 dedented the TA-filter + LLM research + execute block out of the
for perception in results: loop, so only the last (lowest-ranked) trigger
was ever researched per scan. The top-ranked trigger was silently skipped
all cycle - e.g. LINK ranked #1 for 34 straight scans (scores 35-45) but
was first researched only when ACE briefly out-scored it at the top of
LINK's +8.8% spike, producing the 2026-08-15 late-chase entry.

Re-indent the block back into the loop so all confirmed triggers are
researched per cycle in score order. sleep/progress timestamp stay
outside the loop (once per cycle). Restores pre-7e63882 structure plus
the newer PASS/Chronos logging.
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