Conversation
…ng system Hyperliquid mean-reversion perpetual futures trading bot with: - Signal engine (Z-score, Bollinger, RSI, ADX) with prediction market regime modifiers - Paper and live execution with limit-first/taker-fallback strategy - Risk management (stop-loss, daily limits, cooldowns, position sizing) - Backtesting with realistic cost model and walk-forward analysis - Unix socket IPC for daemon-TUI communication - Textual TUI dashboard with live monitoring and key-bound commands - 167 tests passing, zero lint errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a full-featured Hyperliquid perpetual futures mean-reversion bot, including an IPC layer and a Textual-based TUI dashboard for live monitoring/control, plus supporting modules for signals, risk, execution, backtesting, and reporting.
Changes:
- Introduces Unix-socket IPC for daemon state + command dispatch and a Textual TUI dashboard (pause/resume/emergency close).
- Implements core trading components: indicators + signal engine, risk manager, Hyperliquid WS client, and a live execution layer.
- Adds a backtesting framework (fees/slippage/funding, walk-forward, sensitivity) and extensive test coverage.
Reviewed changes
Copilot reviewed 63 out of 75 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/init.py | Marks tests package. |
| tests/test_alerts.py | Tests Discord/Telegram alert sending (mocked). |
| tests/test_backtest_cost_model.py | Tests fee/slippage/funding cost models. |
| tests/test_backtest_engine.py | Integration tests for backtest engine/executor/risk adapter. |
| tests/test_backtest_metrics.py | Tests backtest metric calculations. |
| tests/test_indicators.py | Tests self-implemented technical indicators. |
| tests/test_ipc.py | Tests IPC protocol/state/server/client. |
| tests/test_live_executor.py | Tests LiveExecutor behavior with mocked SDK. |
| tests/test_prediction.py | Tests prediction-market scoring + regime integration. |
| tests/test_risk.py | Tests RiskManager behavior and cooldown persistence. |
| tests/test_signals.py | Tests SignalEngine behavior. |
| tests/test_tui.py | Tests widget formatting/helpers and basic state updates. |
| tests/test_ws_client.py | Tests WsClient subscriptions/caching/health/reconnect. |
| src/perp_bot/backtest/init.py | Backtest module exports. |
| src/perp_bot/backtest/config.py | Re-exports BacktestConfig. |
| src/perp_bot/backtest/cost_model.py | Fee/slippage/funding models for backtests. |
| src/perp_bot/backtest/engine.py | Core backtest engine simulation loop. |
| src/perp_bot/backtest/executor.py | In-memory backtest executor. |
| src/perp_bot/backtest/metrics.py | Backtest performance metrics. |
| src/perp_bot/backtest/results.py | Backtest result dataclasses + CSV export. |
| src/perp_bot/backtest/risk_adapter.py | Deterministic backtest risk manager. |
| src/perp_bot/backtest/sensitivity.py | Parameter sensitivity sweep. |
| src/perp_bot/backtest/walk_forward.py | Walk-forward analysis runner. |
| src/perp_bot/config.py | Centralized config schema + YAML/.env loader. |
| src/perp_bot/data/init.py | Data module marker. |
| src/perp_bot/data/client.py | Hyperliquid REST client wrapper. |
| src/perp_bot/data/db.py | SQLite schema + persistence helpers (candles/trades/predictions/state). |
| src/perp_bot/data/ingest.py | Data ingestion orchestration (candles/funding/predictions). |
| src/perp_bot/data/prediction_client.py | Polymarket + Kalshi fetchers. |
| src/perp_bot/data/ws_client.py | Hyperliquid WebSocket client wrapper with caches/subscriptions. |
| src/perp_bot/execution/init.py | Execution module marker. |
| src/perp_bot/execution/executor.py | Executor interface + PaperExecutor. |
| src/perp_bot/execution/live_executor.py | Live Hyperliquid execution (limit-first + SL + fallback + slippage stats). |
| src/perp_bot/infra/init.py | Infra module marker. |
| src/perp_bot/infra/alerts.py | Discord/Telegram alert dispatch. |
| src/perp_bot/infra/health.py | Periodic heartbeat/health checker. |
| src/perp_bot/infra/logging.py | JSON logging + optional rotating file handler. |
| src/perp_bot/ipc/init.py | IPC module marker. |
| src/perp_bot/ipc/client.py | Unix socket daemon client. |
| src/perp_bot/ipc/protocol.py | IPC constants + socket path helper. |
| src/perp_bot/ipc/server.py | Threaded Unix socket server for state/commands. |
| src/perp_bot/ipc/state.py | Thread-safe daemon state container. |
| src/perp_bot/reporting/init.py | Reporting module marker. |
| src/perp_bot/reporting/compare.py | Paper-vs-backtest comparison report. |
| src/perp_bot/reporting/weekly.py | Weekly performance report. |
| src/perp_bot/risk/init.py | Risk module marker. |
| src/perp_bot/risk/manager.py | Live risk manager (daily loss, cooldown, sizing, stop-loss). |
| src/perp_bot/signals/engine.py | Signal evaluation logic + regime adjustments. |
| src/perp_bot/signals/indicators.py | Self-implemented indicators (zscore/BB/RSI/Hurst/ADX). |
| src/perp_bot/signals/prediction.py | Prediction-market scoring + regime classification. |
| src/perp_bot/tui/init.py | TUI module marker. |
| src/perp_bot/tui/app.py | Textual app wiring (poll daemon/DB/log, actions). |
| src/perp_bot/tui/app.tcss | TUI grid layout and widget styling. |
| src/perp_bot/tui/widgets/init.py | Widget module marker. |
| src/perp_bot/tui/widgets/header.py | Header widget formatting + uptime. |
| src/perp_bot/tui/widgets/log.py | Log tail widget (JSON parsing). |
| src/perp_bot/tui/widgets/position.py | Position widget with uPnL calculation. |
| src/perp_bot/tui/widgets/risk.py | Risk status widget. |
| src/perp_bot/tui/widgets/signals.py | Signals widget with indicator bars. |
| src/perp_bot/tui/widgets/trades.py | Trades DataTable widget. |
| pyproject.toml | Project metadata + dependencies (adds Textual). |
| config.yaml | Default bot configuration incl. prediction/backtest blocks. |
| deploy/deploy.sh | GCP deployment helper script. |
| deploy/perp-bot.service | systemd unit for daemon deployment. |
| hyperliquid-mean-reversion-bot-design.md | Strategy/design document (JP). |
| CLAUDE.md | Repository guidance and command references. |
| .env.example | Example env vars for secrets/alerts. |
| .gitignore | Ignores venv, secrets, DBs, caches. |
| .python-version | Pins local Python version for tooling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_update_ignores_private_fields(self): | ||
| state = DaemonState() | ||
| state.update(_lock="hacked") | ||
| import threading | ||
| assert isinstance(state._lock, threading.Lock) | ||
|
|
| def poll_log(self) -> None: | ||
| """Read new lines from the log file since last poll.""" | ||
| if not self._log_path.exists(): | ||
| return | ||
|
|
||
| try: | ||
| with open(self._log_path) as f: | ||
| f.seek(self._last_pos) | ||
| new_data = f.read() | ||
| self._last_pos = f.tell() | ||
| except OSError: | ||
| return | ||
|
|
||
| if not new_data: | ||
| return | ||
|
|
||
| import json | ||
| for line in new_data.strip().split("\n"): | ||
| if not line: | ||
| continue | ||
| try: | ||
| entry = json.loads(line) | ||
| ts = entry.get("ts", "") | ||
| # Extract HH:MM:SS from ISO timestamp | ||
| time_part = ts.split("T")[1][:8] if "T" in ts else ts[:8] | ||
| level = entry.get("level", "INFO") | ||
| msg = entry.get("msg", line) | ||
|
|
||
| level_colors = { | ||
| "WARNING": "yellow", | ||
| "ERROR": "red", | ||
| "CRITICAL": "bold red", | ||
| } | ||
| color = level_colors.get(level, "white") | ||
| self.write(f"[dim]{time_part}[/] [{color}]{msg}[/]") | ||
| except (json.JSONDecodeError, KeyError): | ||
| self.write(line) |
| def _handle_emergency_close(self, request: dict) -> dict: | ||
| symbol = request.get("symbol") | ||
| if not symbol: | ||
| return {"ok": False, "error": "symbol_required"} | ||
|
|
||
| if not self._executor or not self._db: | ||
| return {"ok": False, "error": "executor_not_available"} | ||
|
|
||
| open_trades = self._db.get_open_trades(symbol) | ||
| if not open_trades: | ||
| return {"ok": True, "message": f"no_open_trades_for_{symbol}"} | ||
|
|
||
| closed = 0 | ||
| for trade in open_trades: | ||
| try: | ||
| self._executor.close_position( | ||
| trade["id"], symbol, trade["entry_price"], 0.0, | ||
| "emergency_close_ipc", | ||
| ) |
| pnls = [_get(t, "pnl", 0) for t in trades] | ||
| wins = sum(1 for p in pnls if p > 0) | ||
| net = sum(pnls) | ||
|
|
||
| hold_hours = [] | ||
| for t in trades: | ||
| entry = _get(t, "entry_time", 0) or _get(t, "entry_time_ms", 0) | ||
| exit_ = _get(t, "exit_time", 0) or _get(t, "exit_time_ms", 0) | ||
| if entry and exit_: | ||
| hold_hours.append((exit_ - entry) / 3_600_000) | ||
|
|
||
| return { | ||
| "trades": len(trades), | ||
| "win_rate_pct": wins / len(trades) * 100, | ||
| "net_pnl": net, | ||
| "avg_pnl": net / len(trades), | ||
| "avg_hold_h": sum(hold_hours) / len(hold_hours) if hold_hours else 0, | ||
| } |
| def _query_open_trades(self) -> list[dict]: | ||
| try: | ||
| conn = self._get_ro_connection() | ||
| cur = conn.execute( | ||
| "SELECT * FROM trades WHERE exit_time IS NULL", | ||
| ) | ||
| cols = [d[0] for d in cur.description] | ||
| rows = [dict(zip(cols, row)) for row in cur.fetchall()] | ||
| conn.close() | ||
| return rows | ||
| except Exception: | ||
| return [] | ||
|
|
||
| def _query_recent_trades(self) -> list[dict]: | ||
| try: | ||
| conn = self._get_ro_connection() | ||
| now_ms = int(time.time() * 1000) | ||
| week_ms = 7 * 24 * 3600 * 1000 | ||
| cur = conn.execute( | ||
| "SELECT * FROM trades WHERE exit_time IS NOT NULL" | ||
| " AND exit_time >= ? ORDER BY exit_time DESC LIMIT 10", | ||
| (now_ms - week_ms,), | ||
| ) | ||
| cols = [d[0] for d in cur.description] | ||
| rows = [dict(zip(cols, row)) for row in cur.fetchall()] | ||
| conn.close() | ||
| return list(reversed(rows)) | ||
| except Exception: | ||
| return [] |
| @@ -0,0 +1 @@ | |||
| 3.14 | |||
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| ingestor.update_candles(symbol) | ||
|
|
||
| # Load candles into DataFrame | ||
| candles = db.get_candles(symbol, tf, limit=min_candles + 50) |
There was a problem hiding this comment.
Trading loop fetches oldest candles instead of newest
High Severity
db.get_candles(symbol, tf, limit=min_candles + 50) with no start_time generates a query with ORDER BY open_time ASC LIMIT ?, which returns the oldest candles in the database, not the most recent ones. After a 90-day backfill populates thousands of rows, the signal engine computes indicators on ancient historical data instead of current market conditions. All trading decisions (entries, exits, z-scores, RSI, ADX) are based on stale data from months ago.
Additional Locations (1)
| pnl = (entry_price - current_price) / entry_price * position_size | ||
|
|
||
| max_loss = self.trading.capital_usd * self.risk.max_loss_per_trade_pct | ||
| return pnl <= -max_loss |
There was a problem hiding this comment.
Stop-loss uses computed size, not actual trade size
Medium Severity
check_stop_loss calls self.compute_position_size() to get a hypothetical position size rather than using the actual trade's size_usd. Positions opened under HIGH_RISK regime have half the normal size, but the stop-loss check always uses the full NORMAL-regime size, causing PnL to be overestimated by 2× and triggering premature stop-losses. The backtest's BacktestRiskManager.check_stop_loss correctly accepts size_usd as a parameter.
Additional Locations (1)
| "DOVISH_SHIFT": "yellow", "HAWKISH_SHIFT": "yellow", | ||
| "CRISIS": "red", | ||
| } | ||
| rc = regime_colors.get(regime, "white") |
There was a problem hiding this comment.
Regime color keys don't match lowercase enum values
Low Severity
The regime_colors dict uses uppercase keys ("NORMAL", "HIGH_RISK", "CRISIS"), but _tick returns prediction_regime.value which produces lowercase strings ("normal", "high_risk", "crisis"). After the first tick, the lookup always falls through to the "white" default, so the regime indicator never displays its intended color coding.
Additional Locations (1)
Code reviewFound 2 issues:
perp-bot/src/perp_bot/risk/manager.py Lines 67 to 81 in 9bb138a
Lines 240 to 261 in 9bb138a Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…dent recomputation check_stop_loss was calling compute_position_size() which uses the current prediction regime, not the trade's actual notional. If the regime shifted after entry (e.g. NORMAL→CRISIS), the method returned 0 and PnL was always 0 — the stop loss would never fire. Now accepts size_usd as a parameter so the caller passes the real trade size. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The screen command fetched the oldest 500 candles (ASC + LIMIT) rather than the most recent. Adding descending=True queries ORDER BY DESC then reverses the result to maintain chronological order, ensuring Hurst exponent calculations use current market data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compare_paper_vs_backtest included live trades in the paper bucket, inflating or deflating paper metrics. Now filters on is_paper == 1 so the comparison accurately reflects simulated-only performance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ports Three improvements to main.py: 1. Losing-weeks guard no longer calls sys.exit(1) when live positions are open. Instead sets entries_halted=True and continues managing existing positions, preventing abandoned positions on the exchange. 2. _tick now checks the return value of executor.open_position(). If it returns None (entry failed), the OPEN alert is skipped rather than broadcasting a misleading success message. 3. Backtest CSV export generates per-symbol filenames (trades-ETH.csv, trades-BTC.csv) when backtesting multiple symbols, preventing later symbols from overwriting earlier results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all CLI logic from root main.py into src/perp_bot/cli.py and register a console_scripts entry point (`perpbot`) in pyproject.toml. main.py becomes a thin compatibility shim. The --force flag is now passed as a function parameter instead of scanning sys.argv. Also adds: README with install/usage docs, MIT license, changelog, GitHub Actions CI/release workflows, pyproject.toml metadata (classifiers, URLs, sdist excludes), config.py CWD-relative path resolution, and tests for the new CLI parameter passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
You have used all of your free Bugbot PR reviews. To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial. |


Summary
Architecture
IPC split: Socket carries volatile state (mid prices, signals, WS health). SQLite WAL provides concurrent read access for persistent data (trades, candles, PnL).
New modules
src/perp_bot/ipc/(5 files)src/perp_bot/tui/(9 files)tests/test_ipc.py,tests/test_tui.pyModified files
main.py-- DaemonState + IPC server wired into trade loop, pause logic, signal exposure, tui/status CLI commandssrc/perp_bot/infra/logging.py-- Added log_file param with RotatingFileHandler for TUI log tailingpyproject.toml-- Added textual>=1.0.0 dependencyTest plan
uv run pytest tests/ -v-- all 167 tests passuv run ruff check src/ tests/-- zero errorsuv run python main.py tradestarts daemon with IPC socketuv run python main.py statusreturns JSON state from running daemonuv run python main.py tuirenders dashboard, updates liveGenerated with Claude Code
Note
High Risk
High risk because this introduces live trading execution (signed Exchange API orders, server-side stop-losses, leverage setting) plus automated reconciliation and alerting, where bugs can directly place/close positions or lose funds. Adds many new subsystems (SQLite persistence, WS reconnect logic, IPC/TUI control surface) that affect runtime behaviour and operational safety.
Overview
Implements an end-to-end Hyperliquid mean-reversion perp trading bot with config-driven data ingestion (REST + WebSocket), SQLite persistence, indicator-based signal generation, and risk-gated trading in
paperorlivemode.Adds live execution via
LiveExecutor(limit-first with IOC fallback, leverage setup, server-side stop-loss placement, slippage monitoring) plus startup position reconciliation and alerting (Discord/Telegram) and periodic health heartbeats.Introduces a full backtesting suite (fee/slippage/funding models, walk-forward, sensitivity sweeps, and paper-vs-backtest comparison) and a Unix-socket IPC + Textual TUI to monitor and control the daemon (pause/resume/emergency close, status command, live panels including log tailing).
Written by Cursor Bugbot for commit 9bb138a. This will update automatically on new commits. Configure here.