Releases: keitaj/hyperliquid-bot
v0.27.0
What's New
Features
-
Forager composite-score auto-exclude (#146, #147) — A second-tier auto-exclude path that scores each coin on three dimensions and pauses quoting when the blended score falls below a threshold, separate from the existing markout-driven
auto_exclude. The score combines activity (close-event flow vs. idle), close-quality (maker close rate, with a minimum-closes gate before the dimension is trusted), and cost (per-1K-volume net cost). Each dimension is weighted independently so operators can re-balance without touching the score formula. Opt-in via--forager; tuning via--forager-threshold(default30.0) plus--forager-w-activity / -quality / -cost(defaults0.3 / 0.4 / 0.3). Cooldown and consecutive-strike gates mirror the existingauto_excludesemantics so the two features compose cleanly. The five non-CLI knobs (FORAGER_WINDOW_SECONDS,FORAGER_CHECK_INTERVAL_SECONDS,FORAGER_ACTIVITY_IDLE_MIN_SECONDS,FORAGER_COST_MAX_PER_1K,FORAGER_MIN_CLOSES_FOR_QUALITY) stay env-only to keep the CLI surface manageable. (#147 promotes the previously hardcoded 5-close quality gate to the same env knob, so all formula thresholds are configurable.) -
JSON config as a layered input source (#148, #149) — New
--config <path>CLI flag (repeatable; later files override earlier) andBOT_CONFIG=<path>env var that load a JSON file as a config layer between the dataclass defaults and CLI/env. Final precedence is CLI > env > JSON > defaults, so the rollout is opt-in and pure-additive: existing CLI/env workflows are byte-identical when no--configis supplied. The loader auto-detects flat ({"spread_bps": 10, …}) vs. nested ({"market_making": {"spread_bps": 10, …}}) form; nested keys are flattened against_NESTED_TO_FLAT_MAPso downstreamMMConfig.from_legacy_dict()is unchanged. JSON values under therisknamespace are wired through toConfig.{KEY.upper()}via a new_apply_json_risk_overrideshelper (closes the gap from the initial #148 wiring); CLI risk flags still beat JSON, JSON beats env, and the helper pops applied keys so they don't leak back intostrategy_config. Unknown keys log a typo warning but otherwise pass through; malformed JSON aborts startup with exit code 2 (fail-safe). Anexamples/config.example.jsoncovers the nested form.
Fixes
_STRATEGY_PARAMS/_COMMON_PARAMSat module scope (#150) — Latent bug discovered during round-trip migration-tool verification: PR #148 lazily imports these lists invalidation.strategy_validator.known_market_making_keys()for typo detection, but only_RISK_PARAMSwas promoted to module level in #149 — the other two stayed insidemain(), so any--configinvocation hitImportErrorat startup. This release moves both lists out ofmain()(call-sites unchanged) and adds aTestKnownMarketMakingKeysregression class that pins the contract via direct import + indirect function calls; future demotion will fail CI rather than reach production.
Upgrade Notes
- All changes are backward compatible — defaults preserve existing behaviour:
- Forager is default-disabled. Without
--foragerthe strategy and shutdown paths are byte-identical to v0.26.0. Operators flipping it on are advised to start with the default weights and--forager-threshold 30, observe[forager]log lines for at least one full session, and tighten only after the per-coin score distribution stabilises. - JSON config is opt-in. With neither
--confignorBOT_CONFIGset, the JSON loader is never invoked. When set, the layer sits below env/CLI; existing.env.farming+start_farming*.shflows continue to win on every shared key. - Migration tool: a companion
scripts/env_to_json.pylives in thehip-3-farming-agent-teamrepo — it converts.env.farmingto a nested JSON config, skipping credentials/DEX/framework keys. Useful for staging a JSON deployment without re-typing the existing knob set. - Level 1 risk guardrails (
risk_manager.pyhardcoded values) are untouched in this release.
- Forager is default-disabled. Without
Operator Checklist
- Forager Phase A (default weights,
min_fills=3, score threshold 30): turn on viaFORAGER_ENABLED=true+--forager. Watch the per-coin score component breakdown in the periodic[forager]summary; confirm no coin is scored below threshold purely on a single dimension before tightening any weight. - JSON config Phase A (loader path validation): set
BOT_CONFIG=/abs/path/config.jsonin.env.farmingwhile leaving all CLI flags instart_farming*.shintact. Confirm[config] Loaded N key(s) from …and[config] JSON layer loaded: N key(s) from 1 file(s)appear at startup with no warnings beyond intentional skips. Phase B (stripping CLI flags so JSON drives values) is a separate operator step after a 24h observation window.
v0.26.0
What's New
Features
- Order refresh tolerance (#144, #145) — Opt-in gate that preserves Hyperliquid's price-time queue priority by keeping an existing limit order across cycles when its recorded price has drifted no more than
--refresh-tolerance-bpbasis points from the current ideal price. The cancel still fires immediately when the drift exceeds tolerance, and a safety-net upper bound on age applies independently via--refresh-max-age-seconds(defaultrefresh_interval_seconds * 4). NewOrderTracker.refresh_orders_with_tolerancerecords cumulative kept / cancelled-by-drift / cancelled-by-age counters;_compute_ideal_pricesis the single source of truth for both the run-loop tolerance check and order placement, so the volatility rolling buffer is updated exactly once per cycle. Default0keeps the legacy age-only behaviour. - Per-coin
unrealized_loss_close_bpsoverrides (#143) —--coin-unrealized-loss-overrides "INTC:25,OIL:10"lets the unrealized-loss early-close threshold be tuned per coin (relax on low-vol coins, tighten on volatile ones). Setting a coin to0disables the feature for that coin only. DYNAMIC_AGEclamp observability + close-reasonmax_agefield (#142) — Per-coin clamp distribution (min_clamp/max_clamp/mid/samples) is logged at the dynamic-age summary cadence, and force-close events now carry the effectivemax_agethey fired against so post-mortems can distinguish baseline vs. dynamic-age driven force-closes.- Builder fee approval verification (#139) — After every
approveBuilderFeecall the bot now reads back the fee approval and logs the result, so a stale approval that failed to land is surfaced rather than silently retried each cycle. The retry path itself is unchanged. - USDT0 in
check_balance.pyspot summary (#138) — Cash HIP-3 perps settle in USDT0; the standalone balance script now prints USDT0 alongside USDC and USDH (header updated toSpot (USDC/USDH/USDT0):). Wallets without USDT0 are unaffected.
Tuning
- Lower
auto_excludemin_fillsdefault from 5 to 3 (#140) — The original 5-fill floor was too strict at the default 300s adverse-selection summary window: many low-volume coins never accumulated enough fills to trigger an exclusion even when adverse selection was clearly negative. Three fills is enough signal at the default cadence; explicit--auto-exclude-min-fillsoverrides still apply.
Tests / Internal
- Codify the close-order invariant for
OrderTracker(#141) — Contract tests pin the load-bearing invariant that close orders never enterOrderTracker._tracked_ordersand so are never cancelled byImbalanceGuard/BboGuard/BboVelocityGuard/FillFeedcleanup paths. Prevents regression of the close-side preservation behaviour validated in the 2026-05-01 system review.
Upgrade Notes
- All changes are backward compatible — defaults preserve existing behaviour:
- Refresh tolerance is opt-in via
--refresh-tolerance-bp(default0= age-only). Recommended Phase B value is2(bp); start with1on the most volatile coins to gauge the kept-rate ratio. - Per-coin
unrealized_loss_close_bpsoverrides default to the global value when unset; setting a coin to0disables only that coin's threshold. - The
DYNAMIC_AGEobservability fields are additive — existing log parsers that don't recognise the new keys are unaffected. - Builder fee approval verification is wired into the existing reassert path; failures still log and continue (init never aborts).
auto_excluderemains default-disabled — themin_fills=3change only kicks in when the feature is explicitly enabled via--auto-exclude+--enable-adverse-selection-log.- Level 1 risk guardrails (
risk_manager.pyhardcoded values) are untouched in this release.
- Refresh tolerance is opt-in via
Operator Checklist
- For Phase B rollout of refresh tolerance: set
REFRESH_TOLERANCE_BP=2(and optionallyREFRESH_MAX_AGE_SECONDS) in the deployment env. The farming-team scripts already forward both via the start-session helpers. - After deploying, watch
OrderTracker.get_refresh_stats()(logged eventsCancelled drift/age <side> order ... for ...andkeeping {side} oid=...) to confirm a healthykeptratio (target ~0.3-0.7 in calm markets).
v0.25.0
What's New
Features
- Per-DEX builder fee support (#136) — Adds optional builder-code attachment per HIP-3 DEX so the bot can become eligible for HIP-3 deployer rewards programs that gate on builder-code presence. New
BUILDER_FEES_<DEX>_ADDRESS,BUILDER_FEES_<DEX>_TENTHS_BPS, andBUILDER_FEES_<DEX>_MAX_FEE_RATEenv vars;bulk_place_ordersgroups orders by DEX so each group ships with its matching builder, and standard Hyperliquid coins keep their existing no-builder path. IdempotentapproveBuilderFeeis reasserted at every startup; failures are logged but never abort initialization. - Auto-exclude on consecutive adverse-selection windows (#132) — Opt-in feedback loop that pauses a coin when
AdverseSelectionTrackerreports moderate adverse selection (avg_<window> ≤ --auto-exclude-threshold-bps, default-3.0) for--auto-exclude-consecutivesummary windows in a row (default3, ~15 min at the default 300s interval). The coin auto-resumes after--auto-exclude-cooldownseconds (default1800). Closes a gap left byloss_streak_limit(close-PnL based, weak against slow bleed) anddynamic_offset(capped at +3 bps).
Bug Fixes
-
Distinguish deposits from API failures in
risk_manager(#133) — The >50% single-cycle balance check previously logged every change as "Likely spot API failure" and never refresheddaily_starting_balance, leaving subsequentdaily_loss_limitchecks comparing against a stale baseline. Now dispatches on direction:- Increase → treated as deposit. Logs at
INFOand resetsdaily_starting_balanceto the new value. - Decrease → treated as likely API failure. Logs at
WARNINGand skips the daily-loss check for that cycle.
metrics.total_balanceis never mutated; <50% changes are unchanged; Level 1 hardcoded guardrails are untouched. - Increase → treated as deposit. Logs at
Refactors (internal — no behaviour change)
- MM config dataclass grouping (#128, #129, #130, #131) — Final phase of grouping all CLI/env-driven MM parameters into typed sub-dataclasses on
MMConfig(close timing, imbalance, schedule, dynamic offset, dynamic age, auto-exclude). Legacy flat keys still load viafrom_legacy_dict; argparsedest=is aligned with config keys per Phase 2b. Flat instance attributes onMarketMakingStrategypreserved as aliases so existing references and tests are unchanged. - Dedupe MM coin-override and volatility helpers (#137) — Replaces four inline copies of HIP-3 coin parsing with
coin_utils.parse_coin, extracts a single_lookup_coin_override(full-name → bare-name → default fallback chain) used by_get_coin_offset/_get_coin_spread/_get_coin_size, and shares one_compute_realized_volatility(coin)between_get_volatility_adjusted_offsetand_get_dynamic_position_age.
Docs
- Refresh stale "Phase 1" docstring in
mm_config(#135) and addstrategies/mm_config.pyto the AGENTS.md architecture table (#134).
Upgrade Notes
- All changes are backward compatible — defaults preserve existing behaviour:
- Builder fees are opt-in via env vars; without them, every order ships with
builder=Noneexactly as before. - Auto-exclude is default-disabled — requires both
--auto-excludeand--enable-adverse-selection-logto be active. - The
risk_managerchange preserves Level 1 hardcoded guardrails and never mutatesmetrics.total_balance. - All MM config refactors keep CLI flags, env vars, and flat instance attributes intact.
- Builder fees are opt-in via env vars; without them, every order ships with
v0.24.0
What's New
Features
- Drain mode for graceful pre-shutdown (#127) — New
--drain-flag-fileflag enables flag-file based drain mode for the market-making strategy. When the configured file exists, the strategy stops placing new entry orders and only manages existing positions via the normal maker-first close flow. Designed to be triggered by an external script before SIGTERM so positions can unwind via maker close instead of taker IOC close at session boundaries. Behaviour mirrors the existingquiet_hoursfull-stop mode; drain takes priority when both apply. - Periodic dynamic_age summary log (#125) — Adds a 5-minute interval summary log line that lists each tracked coin's current realized volatility (bps) and the resulting effective
max_position_age(seconds). Provides runtime visibility into how--dynamic-ageis reshaping holding times per coin without requiring debug-level logging.
Bug Fixes
- Race-safe active_orders cleanup (#126) — Replace dict membership check +
delwithdict.pop(key, None)inupdate_order_statusto remove a window where two coroutines could both pass theincheck before either deletes the entry, causing aKeyError. Pure refactor; no observable behaviour change in non-racy paths.
Upgrade Notes
- All changes are backward compatible — defaults preserve existing behaviour:
--drain-flag-filedefaults to empty string (disabled). Existing deployments behave unchanged unless the flag is explicitly set.- The dynamic_age summary log is emitted only when
--dynamic-ageis enabled and at most once every 5 minutes; it adds no measurable overhead. - The
update_order_statuschange is a refactor with no public API or default-path behaviour change.
Operational Tips
- To use drain mode in a session-switch helper, point
--drain-flag-fileat a path the helper cantouchshortly before sending SIGTERM, then remove the file (or have the next bot start clean it up). The bot enters drain on the next main-loop tick after the file appears.
v0.23.0
What's New
Features
- Volatility-adjusted MAX_POSITION_AGE (#124) — New
--dynamic-ageflag adjusts position holding time based on per-coin realized volatility. High-volatility coins get shorter holding times (reducing adverse selection); low-volatility coins get longer times (improving maker fill probability). Configurable via--dynamic-age-baseline-vol,--dynamic-age-min,--dynamic-age-max. - Per-coin order size overrides (#123) — New
--coin-size-overridesflag (e.g.\"TSLA:150,NVDA:150\") allows setting different order sizes per coin. Setting a coin to `0` skips orders for that coin. Follows the same lookup pattern as existing--coin-offset-overridesand--coin-spread-overrides. - Unrealized loss based early taker close (#122) — New
--unrealized-loss-close-bpsflag triggers immediate taker close when a position's unrealized loss exceeds the configured bps threshold, regardless of position age. Targets large-loss trades that accumulate while waiting for maker close.
Upgrade Notes
- All changes are backward compatible — defaults preserve existing behavior:
--dynamic-ageis off by default (boolean flag)--coin-size-overridesdefaults to empty string (all coins use global `--order-size-usd`)--unrealized-loss-close-bpsdefaults to 0 (disabled)
- The dynamic age feature reuses the same
_recent_midsprice history as--vol-adjust, so no extra data collection overhead when both are enabled.
v0.22.0
What's New
Features
- Coin list deduplication at startup (#121) — Automatically deduplicates coin list and logs a warning when duplicates are detected, preventing double-order issues
- Range syntax for spread_schedule (#119) — Supports hour-range shorthand (e.g.
"0-3:1.5") in addition to individual hour entries, making schedule configuration more concise - Close reason classification logging (#118) — Logs structured close reasons (TP/SL/signal/force/manual) for position closes, improving post-session analysis
Improvements
- Extract _TIER_NAMES module constant (#120) — Refactored duplicated tier-name dicts into a single module-level constant for consistency and maintainability
Bug Fixes
- _open_positions guard in close_position (#117) — Added missing guard to prevent reduce-only order rejections when position is already closed
Upgrade Notes
- All changes are backward compatible — no new flags or configuration required
- If your coin list had duplicates, you will now see a warning log at startup instead of silent double-ordering
- Spread schedule now accepts range syntax:
--spread-schedule "0-3:1.5,14-16:1.5"(individual hour syntax still works)
v0.21.0
What's New
Features
- Micro-price asymmetric offset (#109) — Adjusts buy/sell offsets based on top-of-book size imbalance to reduce adverse selection on the riskier side
- BBO velocity guard (#109) — Cancels orders when BBO moves consistently in one direction (3+ consecutive ticks), preventing fills during fast moves
- Close order optimization (#112) — Progressive close pricing with spread tiers, BBO tracking via CloseRefreshGuard for improved maker close rates
- Hourly spread schedule (#113) — Per-hour spread multiplier (
--spread-schedule "14:1.5,20:1.5") to widen spreads during high adverse-selection time windows - Dynamic offset auto-adjustment (#114) — Auto-adjusts per-coin BBO offset based on adverse selection severity from the tracker; widens for high-AS coins, tightens for favorable ones
Bug Fixes
- CloseRefreshGuard activation fix (#110) — Fixed rate limiting and WS reconnect issues that prevented close order refresh from working
- Reduce-only rejection fix (#111) — 3-layer defense with fresh position verification in force-close path (11 errors/session → 2)
- WS reconnect guard re-registration (#115) — Fixed AdverseSelectionTracker, VelocityGuard, and PositionCloser being silently lost on WebSocket reconnection (~3h intervals)
- Final reduce-only guard (#116) — Check
_open_positions(updated instantly by FillFeed) right before all reduce_only orders, eliminating the 2-second cache race condition
Docs
- Updated README with all new parameters and AI YAML reference
Upgrade Notes
- All new features are disabled by default (backward compatible)
- Enable with:
--microprice-skew,--velocity-guard,--dynamic-offset,--spread-schedule "..." - Requires
--enable-wsfor micro-price, velocity guard, and dynamic offset - Requires
--enable-adverse-selection-logfor dynamic offset
v0.20.0
What's New
Features
- Quiet Hours (#101): Skip or widen spread during high adverse-selection UTC hours.
--quiet-hours-utc 17stops quoting during UTC 17;--quiet-hours-spread-multiplier 2.0widens spread instead. - Close Refresh Guard (#102): Refresh close orders when BBO changes, improving maker close fill rate before the taker fallback deadline.
--close-refresh-threshold-bps 1.0 - Per-Coin Spread Overrides (#103): Configure BBO offset and spread per coin.
--coin-offset-overrides "SP500:0.5,MSFT:3"allows tighter spreads for efficient coins and wider spreads for high-taker coins. - Adverse Selection Tracker (#104): Measure mid-price movement 5s/30s/60s after each fill to quantify adverse selection per coin. Observation only.
--enable-adverse-selection-log - Dynamic Close Pricing (#106): Progressive loss acceptance during force-close phase, scaling from
aggressive-loss-bpstoforce-close-max-loss-bpsas position approaches the taker deadline.
Bug Fixes
- Reduce-Only Collision Prevention (#105): Fix race condition causing "Reduce only order would increase position" errors. Three-layer defense: FillFeed→PositionCloser notification, dead OID defer, position verification.
Improvements
- ImbalanceGuard Summary Logging (#107): Periodic summary with max imbalance per coin and cancel/skip counts for operational diagnostics.
- README Update (#108): Document all new CLI flags in both human-readable and AI YAML reference sections.
Stats
- 8 PRs merged
- 100 new tests (544 → 630 total)
- 2,550 lines added
v0.19.0
What's Changed
Features
- WebSocket fill feed for instant opposite-side cancellation (#96)
- BboGuard for instant stale quote cancellation on BBO change (#97)
- ImbalanceGuard for reactive one-sided order cancellation on L2 skew (#98)
- WebSocket auto-reconnect on connection drop (#99)
Refactoring
- Downgrade bulk cancel partial failure logs from WARNING to DEBUG (#100)
Bug Fixes
- Create separate WS-enabled Info for WebSocket feed
Full Changelog: v0.18.0...v0.19.0