Conversation
5a9ff35 to
f717679
Compare
1b04747 to
a06a624
Compare
|
@claude review this PR |
|
Claude finished @Aviksaikat's task in 3m 8s —— View job PR #770 review: feat: cancel orders (limit)
SummaryThis PR adds cancellation support for pending limit orders (stop loss, take profit, limit increase) on GMX V2. It spans the full stack: low-level transaction building, CCXT adapter integration, Freqtrade exchange class, tests, and example scripts. It also includes several bug fixes (nonce recovery, gas pricing, Subsquid endpoint updates, execution buffer adjustment, liquidation guard). The architecture is well thought out — Issues1. Duplicate code:
|
PR Review: feat: cancel orders(limit)Good work on a substantial feature. The cancel order lifecycle is well thought out with proper CCXT compatibility, batch support, and freqtrade integration. Here is detailed feedback: Strengths
Issues to address1. Code duplication between sync and async
|
aa4bd32 to
c071707
Compare
|
@claude review now |
|
Claude finished @Aviksaikat's task in 4m 48s —— View job PR #770 review: feat: cancel orders (limit)
SummaryThis PR adds cancellation support for pending limit orders (stop loss, take profit, limit increase) on GMX V2 across the full stack: low-level transaction building, CCXT adapter, Freqtrade exchange class, GraphQL client, tests, and example scripts. It also bundles several important fixes: nonce recovery, gas pricing, execution buffer correction, Subsquid endpoint swap, and a liquidation guard. The core architecture is solid — Issues1.
|
Code review: feat: cancel orders(limit) (#770)Overall this is a substantial feature addition (~4300 lines added) that implements GMX limit order cancellation across the core order module, CCXT wrappers (sync/async), freqtrade integration, a new GraphQL/Subsquid client, and several scripts/tests. Below are the findings. Potential bugs1. is_long = side != "sell" # we were trying to close a long (sell side)The comment says "we were trying to close a long (sell side)" — but that logic sets 2. loop = asyncio.get_event_loop()This is deprecated since Python 3.10 and raises 3. Timestamp inconsistency in "timestamp": self.milliseconds(),
"datetime": self.iso8601(self.milliseconds()),These could yield slightly different values. Capture once in a local variable (as is already done correctly in 4. return self.trigger_price / 10**12The docstring acknowledges this is for "standard 18-decimal index tokens", but for BTC (8 decimals) the divisor should be 5. Race condition in nonce recovery Code quality6. Significant duplication between sync and async exchange 7. Inconsistent execution_buffer = params.get("execution_buffer", self.options.get("executionBuffer", 2.2))The sync version uses 8. Inconsistent order_type = "stopLoss" # camelCase
order_type = "take_profit" # snake_caseThe comment explains camelCase for stop-loss is to avoid Telegram MarkdownV1 escaping issues, but this inconsistency forces downstream code to check for both 9. 10. 11. Duplicated utility functions across scripts Security12. Telegram token could leak in error logs 13. Direct nonce manipulation bypasses wallet safeguards Performance14. 15. Test coverage gaps16. No tests for async 17. No tests for nonce recovery logic — 18. No tests for gas-critical throttling — The sliding window, pause mechanism, and Telegram alerting in 19. No tests for 20. No tests for 21. Test removed without replacement — Style issues (per CLAUDE.md conventions)22. Missing type hints
23. Emojis in logger calls 24. 25. SummaryThe core cancellation logic looks solid and well-structured. The main concerns are:
Nice work on the comprehensive implementation — the cancel order flow, pending order fetching, and freqtrade integration are well thought out. The test fixtures for Anvil fork-based testing are well designed. |
6398e74 to
8fe2048
Compare
Expands the warning docstring with the root cause confirmed via Tenderly transaction debugger: during executeOrder, GMX's settlement logic transfers the wallet's entire ETH balance to a settlement contract because the wallet is impersonated (anvil_impersonateAccount) via unlocked_addresses. Includes the exact Tenderly balance-change trace as inline evidence and confirms this is a fork-only artefact safe in production. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test assumed a live GMX API call would always produce a price deviation large enough to trigger PriceSanityException, but check_price_sanity has a fail-open design: if either the oracle or ticker price extraction fails (network timeout, rate limit, etc.) it returns passed=True. This silently suppressed the exception in CI, causing the pytest.raises assertion to fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
createLimitBuyOrder and createLimitSellOrder delegate to the implemented create_limit_order() so advertise them as True. fetchClosedOrders explicitly raises NotSupported so mark it False instead of None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The :prod alias for gmx-synthetics-arbitrum stopped serving data queries (introspection returns 200 but all data queries hang). GMX interface now uses the versioned hash @5acc9d; updated primary endpoint accordingly. The old :prod URL is retained as a backup in case it is restored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Nonce recovery (exchange.py)** Add `_send_signed_tx_with_nonce_recovery()` helper that wraps `send_raw_transaction` with a single auto-retry on nonce mismatch: - "nonce too high": resyncs `wallet.current_nonce` from `get_transaction_count(address)` (latest confirmed) and retries. - "nonce too low": resyncs from `get_transaction_count(address, "pending")` (confirmed + mempool, avoids reusing a queued nonce) and retries. - All other `Web3RPCError` re-raised unchanged. - Retry failure re-raised with full context for easier production debugging. Wire the helper into all three affected call sites: - `create_order`: replaced the old "nonce too high"-only handler. - `cancel_order`: previously had no nonce error handling at all. - `cancel_orders`: previously had no nonce error handling at all. **Gas-pause overhaul (gmx_exchange.py)** The old `count >= threshold` condition fired a Telegram alert on every bot loop after the threshold (count=3, 4, 5 … 8 seen in production). Changes: - Three module-level constants: `_GAS_CRITICAL_MAX_RETRIES=3`, `_GAS_CRITICAL_WINDOW_SECS=300`, `_GAS_CRITICAL_PAUSE_SECS=900`. - `_gas_critical_attempts` state extended to a 3-tuple `(count, window_start, paused_until)`. - Pre-flight guard at the top of `create_order` (before `super().create_order()`): raises `InsufficientFundsError` immediately when the pair is within its pause window — no GMX API call, no Telegram. Clears state and resumes automatically once the pause expires. - `count == threshold`: sets `paused_until = now + 900s`, sends Telegram **once**, raises. - `count > threshold`: silent debug log + raise (pre-flight handles subsequent loops). - Telegram message updated to say "paused for 15 minutes" (now accurate). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…close, liquidation guard **cancel_order execution_buffer (cancel_order.py, trading.py, async_support/exchange.py)** - `cancel_order()` and `cancel_orders()` now accept an `execution_buffer` param (default `DEFAULT_EXECUTION_BUFFER`) threaded through `CancelOrder._build_cancel_transaction()` and `GMXTrading`. - `cancel_all_pending_orders()` also exposes the param. - Async `cancel_order` resolves tx_hash → order_key via the orders cache (same pattern as the sync version), and logs `execution_buffer`. - Async `cancel_orders` batch method added (was sync-only before). **Gas price fix for cancels and SL/TP orders (cancel_order.py, sltp_order.py, execution_buffer.py)** - Cancel transactions now price gas as `web3.eth.gas_price * execution_buffer` instead of `estimate_gas_fees()`, which returns an inflated `maxPriorityFeePerGas` (~1.5 gwei vs ~0.01 gwei base fee on Arbitrum) and was inflating cancel costs ~100x. - `SLTPOrder` calculation uses `web3.eth.gas_price` for the same reason. - `DEFAULT_SLTP_EXECUTION_FEE_BUFFER` lowered 3.0 → 1.0: the compound multiplier caused SL/TP fees to be ~5x higher than the GMX UI; setting it to 1.0 applies a single execution buffer consistent with all other order types. **Subsquid position-close query (graphql/client.py)** - `GMXSubsquidClient.get_latest_position_close()` polls Subsquid for the most recent decrease event for a given account/market. Used when freqtrade tries to close a position that was already closed (stop-loss or liquidation) so the actual execution price, fees, and close reason can be returned instead of synthetic guesses. **Liquidation price guard and constant (utils.py, constants.py)** - `GMX_MIN_LIQUIDATION_COLLATERAL_USD = 5.0` extracted as a named constant. - `calculate_estimated_liquidation_price()` now returns `0.0` when `max_loss <= 0` (collateral below the $5 liquidation threshold) instead of producing a nonsensical price above entry (long) or below entry (short). A `0.0` sentinel is treated as falsy by freqtrade and falls back to the strategy's stop-loss price. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the inline Telegram HTTP logic out of _send_gas_critical_telegram_alert() into a standalone send_freqtrade_telegram_message(config, message) function in eth_defi/gmx/freqtrade/telegram_utils.py. The new utility can be imported and called by any future alert (liquidation warning, nonce exhaustion, etc.) with a single line, keeping message-specific logic in the callers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'stop_loss' has underscores that Telegram MarkdownV1 treats as italic markers, causing 'Can't parse entities' errors. 'stopLoss' avoids this while being more descriptive than the generic 'stop'. Updated everywhere that reads the fetched type: - _format_pending_order(): "stop" → "stopLoss" - exchange.py cancel recovery guard: "stop" → "stopLoss" - gmx_exchange.py duplicate-SL guard: "stop" → "stopLoss" - test_ccxt_cancel_order.py: assertions updated - debug_ccxt_cancel_order.py: assertion updated Input types for create_order dispatch (type="stop_loss") are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, docs Concrete bug fixes: - _send_signed_tx_with_nonce_recovery: make internal retry_tx copy instead of mutating the caller's transaction dict (issue 1) - cancel_orders (sync + async): move config.get_chain() before the loop — it never changes per iteration (issue 2) - async cancel_order/cancel_orders: replace deprecated asyncio.get_event_loop() with asyncio.get_running_loop() (correct inside async functions, issue 4) Documentation / clarity fixes: - PendingOrder.trigger_price_usd: prominent warning that it assumes 18-decimal tokens; add trigger_price_usd_for_decimals(token_decimals) helper (issue 3) - cancel_orders: document O(N) sequential RPC validation overhead (issue 11) - get_latest_position_close: document that it blocks the calling thread (issue 9) - _GMX_ORDER_TYPE_NAMES: explain why OrderType enum can't replace it directly - _strip_json_comments: note that inline trailing comments are not handled - graphql/client.py: remove "ChatGPT" comment, replace with neutral wording - gmx_exchange.py module docstring: update to reflect SL/TP cancel support Re-exports: - eth_defi/gmx/order/__init__.py: restore common re-exports (OrderResult, CancelOrder, PendingOrder, etc.) so from eth_defi.gmx.order import X works Scripts: - cancel_gmx_orders.py: guard rich import with clear ImportError message - tests/gmx/lagoon: add @flaky to all 4 integration tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… first Both cancel_order and cancel_orders in the async GMX exchange class were reading execution_buffer only from params then self.options, skipping the instance attribute set at construction time. This meant passing execution_buffer=1.5 at init was silently ignored by the async cancel path. Fix uses getattr(self, "execution_buffer", fallback) to probe the instance attribute before falling back to self.options, matching the sync behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s effective Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s are valid Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… fix test classes - Add _get_logs_adaptive() (sync) and _get_logs_adaptive_async() (async) helpers that catch 'logs matched by query exceeds limit' ExtraValueError and recursively bisect the block range down to _MIN_LOG_CHUNK_BLOCKS=100 before re-raising - Use helpers in _scan_logs_chunked_for_trade_action and async variant so busy EventEmitter blocks are never silently skipped - Convert test_cancel_helpers.py test classes to standalone functions (CLAUDE.md rule) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_utils.py Both gmx_ccxt_limit_order_cancel.py and gmx_limit_order_cancel.py contained an identical private _fetch_eth_spot_price() function. Extract it into a new shared module scripts/gmx/script_utils.py and update both scripts to import from there. Also add __init__.py to scripts/ and scripts/gmx/ to support the package import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ls docstring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…c signature to multi-line Adds trailing commas so ruff format keeps these expanded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ckup :prod is the official GMX docs URL; @5acc9d versioned hash is retained as the fallback in case :prod becomes unresponsive again. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When running against an Anvil fork, Subsquid (a live-chain indexer) may return a transaction that matches an order key prefix from mainnet history but does not exist on the fork. _build_result_from_subsquid_action() previously caught the TransactionNotFound exception and still returned a non-None OrderStatusResult(execution_receipt=None), which caused check_order_status() to short-circuit and skip the on-chain RPC log scan fallback. As a result fetch_order() kept the cached "open" status instead of detecting the cancellation, causing test_ccxt_fetch_order_detects_cancelled to fail with "Expected status 'cancelled', got: open". Fix: return None from _build_result_from_subsquid_action() when the receipt fetch raises an exception. A None return causes check_order_status() to fall through to _scan_event_emitter_logs_chunked(), which correctly finds the OrderCancelled event in the fork's EventEmitter logs. This also improves production robustness: transient receipt-fetch failures no longer leave an order permanently stuck as "open". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all module-level constants that were spread across the GMX module into the central eth_defi/gmx/constants.py: - Gas-critical pause constants (_GAS_CRITICAL_MAX_RETRIES, _GAS_CRITICAL_WINDOW_SECS, _GAS_CRITICAL_PAUSE_SECS) from freqtrade/gmx_exchange.py - Execution buffer thresholds (DEFAULT_EXECUTION_BUFFER, DEFAULT_SLTP_EXECUTION_BUFFER, DEFAULT_SLTP_EXECUTION_FEE_BUFFER, EXECUTION_BUFFER_CRITICAL_THRESHOLD, EXECUTION_BUFFER_WARNING_THRESHOLD, EXECUTION_BUFFER_RECOMMENDED_MIN, EXECUTION_BUFFER_RECOMMENDED_MAX) from execution_buffer.py - _MIN_LOG_CHUNK_BLOCKS (was duplicated in both ccxt/exchange.py and ccxt/async_support/exchange.py) execution_buffer.py re-exports all constants via import so existing callers (trading.py, order/*.py) are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cancel_helpers.py: minor fix - cancel_order.py: execution buffer import cleanup - cancel_gmx_orders.py: script improvements - test_cancel_helpers.py: additional coverage - test_ccxt_price_sanity.py: test cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add all missing public types and functions to the order module's __init__.py so callers can import everything from one place: New exports: SwapOrder, BaseOrder, OrderParams, CancelOrderResult, BatchCancelOrderResult, SLTPEntry, SLTPParams, SLTPOrderResult, OrderArgumentParser, fetch_pending_order_count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move validate_execution_buffer() and apply_execution_buffer() into gas_utils.py alongside the other gas calculation utilities. The constants were already in constants.py; the functions were the only remaining content. Update all 7 import sites: - DEFAULT_EXECUTION_BUFFER / DEFAULT_SLTP_EXECUTION_FEE_BUFFER now imported from eth_defi.gmx.constants - apply_execution_buffer / validate_execution_buffer now imported from eth_defi.gmx.gas_utils Remove eth_defi.gmx.execution_buffer entry from Sphinx docs index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_gm_prices GMX Reader's getMarketTokenPrice returns a signed int256 and does not revert when pool value is negative. The OM/MANTRA pool went insolvent after the ~90% crash in April 2025, making both checks incorrect: - `!= 0` wrongly treated a bug as valid (negative is not non-zero by accident) - `pytest.approx(rel=0.01)` cross-type comparison fails because FOR_TRADERS, FOR_DEPOSITS and FOR_WITHDRAWALS caps produce legitimately different prices for distressed pools (OM traders≈-27.04 vs deposits≈-26.55, ~1.8% spread) The only invariant that must hold is that each price is a finite float, so all value-range assertions are replaced with math.isfinite() checks. Well-known healthy markets (ETH/BTC/ARB/LINK) in test_get_gm_prices_specific_markets still assert price > 0, as a negative price there would indicate a real problem. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d57597a to
a70da39
Compare
ref: #661