mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-24 20:58:05 +00:00
fix(trading): price tp/sl exits off the trigger price and retry a reverted sell (#193)
* 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b367191267
commit
a0540fdc9e
@@ -191,6 +191,36 @@ with `BuybackFeeRecipientMissing` (6062) printed as confirmed buys.
|
||||
and `str()` on it is empty, so the caller logs a blank reason. A slow
|
||||
`getAccountInfo` is enough to take down a whole listener run this way.
|
||||
|
||||
### Verifying the tp/sl exit path (issue #189)
|
||||
|
||||
```bash
|
||||
# Offline: the exit sell prices off the price that triggered it, a reverted
|
||||
# exit sell is retried, and the retry is bounded
|
||||
uv run learning-examples/verify_tp_sl_exit_price.py
|
||||
```
|
||||
|
||||
`PlatformAwareSeller.execute` does not read a price — the `token_price` it is
|
||||
handed **is** the slippage floor (`min_quote_output = amount * price *
|
||||
(1 - slippage)`). So the caller owns the floor's correctness. A tp/sl exit fires
|
||||
precisely because price left `entry_price`, so pricing the sell off the entry
|
||||
sets a floor the pool cannot pay on a stop-loss and the sell reverts with 6003
|
||||
`TooLittleSolReceived` — during the drop the stop-loss exists to escape. On a
|
||||
take-profit the same mistake runs the other way and the floor protects nothing.
|
||||
`_monitor_position_until_exit` already fetches `current_price` at the top of
|
||||
each iteration, so passing it costs no extra RPC call; `_handle_time_based_exit`
|
||||
genuinely has nothing fresher and keeps passing the buy price.
|
||||
|
||||
The seller's `max_retries` covers **transaction submission only**. An on-chain
|
||||
revert comes back as `success=False` and is not retried there, so the retry has
|
||||
to happen in the monitor loop, where the price is re-read first.
|
||||
`trade.max_exit_sell_attempts` (default 3, validated to 1..100) bounds it so a
|
||||
token that keeps reverting cannot pin the bot on one position, and the counter
|
||||
resets if the price recovers out of the exit band. After the last attempt the
|
||||
position is left open and unmonitored — logged loudly, since the tokens are
|
||||
still held. Watch the `break`: before #189 it sat outside both branches of
|
||||
`if sell_result.success:`, so a failed sell abandoned the position after a
|
||||
single try while leaving `is_active=True`.
|
||||
|
||||
### Listener and decoder pitfalls
|
||||
|
||||
Each of these was a live bug in `learning-examples/`, all of them invisible
|
||||
|
||||
Reference in New Issue
Block a user