* fix(trading): price tp/sl exits off the trigger price and retry a reverted sell
`_monitor_position_until_exit` handed `position.entry_price` to the sell while
the `current_price` that had just triggered the exit sat in the same scope, one
line up. `PlatformAwareSeller.execute` does not read a price - the `token_price`
it receives *is* the slippage floor - so a stop-loss priced off the entry demands
more quote asset than the curve can pay and reverts with 6003
`TooLittleSolReceived`, during the very drop the stop-loss exists to escape. On
a take-profit the same mistake runs the other way and the floor protects
nothing. `current_price` costs no extra RPC call; `_handle_time_based_exit` has
nothing fresher and keeps passing the buy price.
The `break` also sat outside both branches of `if sell_result.success:`, so the
loop exited whether the sell landed or not - contradicting the "Keep monitoring
in case sell can be retried" comment directly above it. The seller's
`max_retries` covers transaction submission only, so an on-chain revert was
never retried and the position was abandoned mid-crash with `is_active=True`.
A failed exit sell now retries on the next price check, re-reading the price so
the floor tracks the market, bounded by `trade.max_exit_sell_attempts`
(default 3, validated to 1..100) so a permanently reverting token cannot pin the
bot on one position. The counter resets if the price recovers out of the exit
band, and giving up is logged loudly since the tokens are still held.
Fixes#189
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(learning-examples): machine-check the tp/sl exit price and retry path
Drives the real `_monitor_position_until_exit` with a stub curve manager
serving a scripted price series and a stub seller that records the price it is
handed. Offline, no network and no funds moved, per the `verify_*` convention.
Eight checks: both exit kinds sell at the triggering price, the entry-price
floor is arithmetically unpayable on a drop while the trigger-price floor is
payable, a reverted sell is retried and a landing retry closes the position,
retries stay bounded, a price recovery resets the counter, a successful sell
still closes on the first attempt, and the cap comes from
`trade.max_exit_sell_attempts` wired through bot_runner and config_loader.
Mutation-tested rather than trusted on a green run - reintroducing the stale
entry price drops it to 4/8, giving up after one failure to 4/8, and ignoring
the config knob to 7/8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: document the tp/sl exit path and its verifier
The caller owns the sell's slippage floor, which is not obvious from
`PlatformAwareSeller.execute` - it never reads a price, it just turns
`token_price` into `min_quote_output`. Records that, why an exit must price off
the triggering price, and that the seller's `max_retries` covers submission
only so an on-chain revert has to be retried in the monitor loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extreme-fast mode submits with no RPC calls between detection and buy
when the listener parsed the on-chain CreateEvent (geyser/logs/blocks):
TokenInfo.state_from_event carries the canonical creator, mayhem/cashback
flags and quote_mint, so the pre-buy curve refresh is skipped. The geyser
listener now delegates to the event-first parser instead of inlining
instruction decoding, which also catches create_v2 transactions that omit
the trailing is_cashback_enabled byte.
Listeners without event data (pumpportal) keep the refresh, hardened per
issue #170: bonding curve derived from the mint instead of the payload's
bondingCurveKey, curve and mint read in one slot-consistent
getMultipleAccounts (correcting the guessed token program), and an
unreadable curve now skips the token within trade.curve_refresh_budget
instead of submitting a buy built from guessed accounts.
Machine-checked by verify_extreme_fast_zero_rpc.py and
verify_pumpportal_buy_path.py; validated live via simulate_bot_buy_path
on both geyser and pumpportal paths.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(core): disable account_data_size limit and add meta.err checking
The 12.5MB setLoadedAccountsDataSizeLimit was causing all buy transactions
to fail with MaxLoadedAccountsDataSizeExceeded since pump.fun migrated to
Token-2022. Transactions landed on-chain (fees paid) but inner instructions
were rejected — the bot paid gas for nothing.
Changes:
- Disable account_data_size in all bot configs (Token-2022 needs >12.5MB)
- Add meta.err check in get_buy_transaction_details() for clear error
reporting when transactions fail on-chain
- Include tx signature in platform_aware.py error messages for debugging
Tested: full buy→sell→cleanup cycle works on pump_fun with geyser listener.
* fix(core): check meta.err in confirm_transaction to detect failed txs
Solana transactions can be "confirmed" (included in a block) but still
fail execution if inner program instructions are rejected. Previously,
confirm_transaction only checked that the tx landed on-chain, causing
silent failures in buy, sell, and cleanup operations.
Now fetches the transaction result after confirmation and checks
meta.err, returning False when the transaction failed. This fixes
the ATA cleanup issue where sells were silently failing with
Custom: 6003 errors, leaving non-zero token balances.
* fix(core): treat failed tx fetch as unconfirmed in confirm_transaction
When _get_transaction_result returns None (RPC failure, timeout, etc.),
the function was falling through to return True — silently treating an
unknown state as success. Now returns False with a warning log.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(core): add RPC rate limiting, retry logic, and 429 handling
Addresses #44 — users on free-tier RPC endpoints hit HTTP 429 errors
during buy transactions due to no rate limiting or retry handling.
- Add TokenBucketRateLimiter (new file: src/core/rpc_rate_limiter.py)
- Gate all RPC methods through rate limiter (both post_rpc and solana-py calls)
- Rewrite post_rpc() with retry loop, exponential backoff, jitter, and
specific 429 detection with Retry-After header support
- Replace per-call aiohttp session with shared persistent session
- Wire node.max_rps from YAML bot config through to SolanaClient
- Fix cleanup manager and learning example to use SolanaClient abstraction
instead of bypassing it via get_client()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(core): address CodeRabbitAI review feedback on rate limiting PR
Validate max_rps > 0 in TokenBucketRateLimiter to prevent ZeroDivisionError
and infinite loops with fractional values. Add asyncio.Lock to _get_session
to fix race condition, handle non-numeric Retry-After headers gracefully,
replace dead json.JSONDecodeError with aiohttp.ContentTypeError, and combine
burn+close into a single transaction in cleanup example.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: document RPC rate limiting feature in README
- Add section on built-in RPC rate limiting with token bucket algorithm
- Document configurable max RPS and automatic retry logic
- Update roadmap to mark "Configurable RPS" as completed
- Clarify benefits of rate limiting for provider compliance
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: use math.ceil for burst_size to handle fractional max_rps
- Replace int(max_rps) with math.ceil(max_rps) in burst_size calculation
- Prevents infinite loop when max_rps < 1.0 (e.g., 0.5 RPS would result in burst_size=0)
- Ensures burst_size is always at least 1 for valid fractional rates
- Addresses CodeRabbit feedback on rpc_rate_limiter.py:27
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(core): validate burst_size and fix table pipe consistency
Address remaining CodeRabbitAI review feedback: add burst_size
validation guard, fix TRY003 lint (use msg variable for ValueError),
break long line under 88 chars, and fix MD055 table pipe style in README.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(core): separate 429 retry budget from error retries in post_rpc
429 responses no longer count against max_retries — they use a dedicated
max_429_retries counter (default 10) so free-tier users hitting rate
limits won't exhaust retries prematurely. Also refresh the aiohttp
session inside the retry loop to avoid stale references after network
failures, and fix cleanup log message accuracy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Anton Sauchyk <antonsauchyk@gmail.com>
* feat(trading): enhance sell execution with token amount and price parameters to reduce RPC delays
* feat(trading): add method to fetch actual token balance after transaction to improve accuracy
* feat(trading): update account data size limit and enhance price calculations